Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,10 +19,10 @@ backend-java/.idea/
|
|||||||
backend-java/*.iml
|
backend-java/*.iml
|
||||||
|
|
||||||
# ===== backend / app =====
|
# ===== backend / app =====
|
||||||
|
__pycache__/
|
||||||
backend/tmp/
|
backend/tmp/
|
||||||
backend/__pycache__
|
backend/__pycache__
|
||||||
app/__pycache__
|
app/__pycache__
|
||||||
app/assets/
|
|
||||||
app/new_web_source
|
app/new_web_source
|
||||||
app/user_data/
|
app/user_data/
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
@@ -20,7 +21,7 @@ except ImportError:
|
|||||||
STATUS_OK = "0"
|
STATUS_OK = "0"
|
||||||
STATUS_LOGIN_FAILED = "-10003"
|
STATUS_LOGIN_FAILED = "-10003"
|
||||||
|
|
||||||
DEFAULT_SOCKET_PORT = 19890
|
DEFAULT_SOCKET_PORT = 20000
|
||||||
CLIENT_API_TIMEOUT = 120
|
CLIENT_API_TIMEOUT = 120
|
||||||
PORT_CHECK_TIMEOUT = 2
|
PORT_CHECK_TIMEOUT = 2
|
||||||
UPDATE_CORE_RETRY_DELAY = 2
|
UPDATE_CORE_RETRY_DELAY = 2
|
||||||
@@ -30,6 +31,7 @@ CLIENT_READY_TIMEOUT = 10
|
|||||||
CLIENT_READY_INTERVAL = 0.5
|
CLIENT_READY_INTERVAL = 0.5
|
||||||
CLIENT_POST_START_DELAY = 5
|
CLIENT_POST_START_DELAY = 5
|
||||||
PROCESS_KILL_DELAY = 3
|
PROCESS_KILL_DELAY = 3
|
||||||
|
ZINIAO_WEBDRIVER_LOG_TAIL_LINES = 80
|
||||||
|
|
||||||
DOC_LOAD_TIMEOUT = 30
|
DOC_LOAD_TIMEOUT = 30
|
||||||
COUNTRY_INITIAL_LOAD_TIMEOUT = 120
|
COUNTRY_INITIAL_LOAD_TIMEOUT = 120
|
||||||
@@ -152,6 +154,49 @@ class ZiniaoDriver:
|
|||||||
time.sleep(CLIENT_READY_INTERVAL)
|
time.sleep(CLIENT_READY_INTERVAL)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _redact_ziniao_log_line(line: str) -> str:
|
||||||
|
return re.sub(r'("password"\s*:\s*)"[^"]*"', r'\1"***"', line)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ziniao_webdriver_log_path() -> Optional[str]:
|
||||||
|
appdata = os.getenv("APPDATA")
|
||||||
|
if not appdata:
|
||||||
|
return None
|
||||||
|
filename = f"webdriver.{time.strftime('%Y%m%d')}.log"
|
||||||
|
return os.path.join(
|
||||||
|
appdata,
|
||||||
|
"ziniaobrowser",
|
||||||
|
"instances",
|
||||||
|
"userdata1",
|
||||||
|
"logs",
|
||||||
|
"client",
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _log_ziniao_webdriver_tail(self) -> None:
|
||||||
|
log_path = self._ziniao_webdriver_log_path()
|
||||||
|
if not log_path:
|
||||||
|
logger.warning("无法读取紫鸟 webdriver 日志: APPDATA 环境变量为空")
|
||||||
|
return
|
||||||
|
if not os.path.exists(log_path):
|
||||||
|
logger.warning("紫鸟 webdriver 日志不存在:{}", log_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(log_path, "r", encoding="utf-8", errors="replace") as log_file:
|
||||||
|
lines = log_file.readlines()
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning("读取紫鸟 webdriver 日志失败:{} | {}", log_path, exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
tail_lines = lines[-ZINIAO_WEBDRIVER_LOG_TAIL_LINES:]
|
||||||
|
tail_text = "".join(self._redact_ziniao_log_line(line) for line in tail_lines).strip()
|
||||||
|
if tail_text:
|
||||||
|
logger.error("紫鸟 webdriver 日志尾部({}):\n{}", log_path, tail_text)
|
||||||
|
else:
|
||||||
|
logger.warning("紫鸟 webdriver 日志为空:{}", log_path)
|
||||||
|
|
||||||
def get_zinaio_exe(self, protocol_name: str = "superbrowser"):
|
def get_zinaio_exe(self, protocol_name: str = "superbrowser"):
|
||||||
"""从 Windows 注册表读取紫鸟客户端可执行文件路径.
|
"""从 Windows 注册表读取紫鸟客户端可执行文件路径.
|
||||||
|
|
||||||
@@ -402,7 +447,7 @@ class ZiniaoDriver:
|
|||||||
|
|
||||||
for retry_count in range(CLIENT_START_RETRIES):
|
for retry_count in range(CLIENT_START_RETRIES):
|
||||||
logger.info("第 {}/{} 次尝试启动紫鸟客户端", retry_count + 1, CLIENT_START_RETRIES)
|
logger.info("第 {}/{} 次尝试启动紫鸟客户端", retry_count + 1, CLIENT_START_RETRIES)
|
||||||
subprocess.Popen(cmd)
|
process = subprocess.Popen(cmd)
|
||||||
|
|
||||||
if self._wait_until_client_ready(CLIENT_READY_TIMEOUT):
|
if self._wait_until_client_ready(CLIENT_READY_TIMEOUT):
|
||||||
logger.info("紫鸟客户端启动成功,第 {} 次尝试", retry_count + 1)
|
logger.info("紫鸟客户端启动成功,第 {} 次尝试", retry_count + 1)
|
||||||
@@ -411,6 +456,9 @@ class ZiniaoDriver:
|
|||||||
return
|
return
|
||||||
|
|
||||||
logger.warning("第 {} 次尝试启动失败,10秒内未检测到客户端启动", retry_count + 1)
|
logger.warning("第 {} 次尝试启动失败,10秒内未检测到客户端启动", retry_count + 1)
|
||||||
|
if process.poll() is not None:
|
||||||
|
logger.warning("紫鸟客户端进程已退出,returncode={}", process.returncode)
|
||||||
|
self._log_ziniao_webdriver_tail()
|
||||||
|
|
||||||
logger.error("紫鸟客户端启动失败,已重试 {} 次", CLIENT_START_RETRIES)
|
logger.error("紫鸟客户端启动失败,已重试 {} 次", CLIENT_START_RETRIES)
|
||||||
raise RuntimeError(f"客户端启动失败:重试 {CLIENT_START_RETRIES} 次后仍未成功启动")
|
raise RuntimeError(f"客户端启动失败:重试 {CLIENT_START_RETRIES} 次后仍未成功启动")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import time
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterable, List, Optional
|
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -63,6 +63,9 @@ DELETE_CONFIRM_MODAL_CLICK_SCRIPT = _load_patrol_delete_script("delete_confirm_m
|
|||||||
DELETE_SUCCESS_MESSAGES_SCRIPT = _load_patrol_delete_script("delete_success_messages.js")
|
DELETE_SUCCESS_MESSAGES_SCRIPT = _load_patrol_delete_script("delete_success_messages.js")
|
||||||
COMPLETE_DRAFT_QUICK_VIEW_TAGS_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_tags.js")
|
COMPLETE_DRAFT_QUICK_VIEW_TAGS_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_tags.js")
|
||||||
COMPLETE_DRAFT_QUICK_VIEW_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_diagnostic.js")
|
COMPLETE_DRAFT_QUICK_VIEW_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("complete_draft_quick_view_diagnostic.js")
|
||||||
|
COMPLETE_DRAFT_SELECT_TAG_SCRIPT = _load_patrol_delete_script("complete_draft_select_tag.js")
|
||||||
|
COMPLETE_DRAFT_SELECT_ALL_SCRIPT = _load_patrol_delete_script("complete_draft_select_all.js")
|
||||||
|
COMPLETE_DRAFT_DELETE_SELECTED_SCRIPT = _load_patrol_delete_script("complete_draft_delete_selected.js")
|
||||||
RECOMMENDED_OFFER_PERCENTAGE_OPEN_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_open.js")
|
RECOMMENDED_OFFER_PERCENTAGE_OPEN_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_open.js")
|
||||||
RECOMMENDED_OFFER_PERCENTAGE_ROWS_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_rows.js")
|
RECOMMENDED_OFFER_PERCENTAGE_ROWS_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_rows.js")
|
||||||
RECOMMENDED_OFFER_PERCENTAGE_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_diagnostic.js")
|
RECOMMENDED_OFFER_PERCENTAGE_DIAGNOSTIC_SCRIPT = _load_patrol_delete_script("recommended_offer_percentage_diagnostic.js")
|
||||||
@@ -82,6 +85,10 @@ DELETE_SUCCESS_COUNT_PATTERNS = (
|
|||||||
DELETE_SUCCESS_TEXT_PATTERNS = (
|
DELETE_SUCCESS_TEXT_PATTERNS = (
|
||||||
re.compile(r"一个或多个商品(?:已删除|已经删除)"),
|
re.compile(r"一个或多个商品(?:已删除|已经删除)"),
|
||||||
)
|
)
|
||||||
|
COMPLETE_DRAFT_DELETE_SUCCESS_PATTERNS = (
|
||||||
|
re.compile(r"商品信息草稿(?:已删除|已经删除)"),
|
||||||
|
re.compile(r"已删除\s*\d*\s*个?\s*商品信息草稿"),
|
||||||
|
)
|
||||||
LISTING_STATUS_DELETE_WHITELIST = {
|
LISTING_STATUS_DELETE_WHITELIST = {
|
||||||
"全部",
|
"全部",
|
||||||
"定价问题",
|
"定价问题",
|
||||||
@@ -170,6 +177,231 @@ class InventoryManage(AmamzonBase):
|
|||||||
raise RuntimeError(f"补全草稿快速查看已打开但未读取到标签: {open_result}")
|
raise RuntimeError(f"补全草稿快速查看已打开但未读取到标签: {open_result}")
|
||||||
return parsed_rows
|
return parsed_rows
|
||||||
|
|
||||||
|
def delete_complete_drafts_from_non_whitelisted_tags(
|
||||||
|
self,
|
||||||
|
shop_name: str,
|
||||||
|
country: str,
|
||||||
|
max_delete_operations: Optional[int] = None,
|
||||||
|
trace_id: str = "",
|
||||||
|
delete_conditions: Optional[Iterable[Any]] = None,
|
||||||
|
allowed_statuses: Optional[Iterable[str]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""按补全草稿快速查看的准确标签数量,删除非白名单标签下方商品。"""
|
||||||
|
trace_id = trace_id or _new_trace_id("draft")
|
||||||
|
effective_conditions = delete_conditions if delete_conditions is not None else allowed_statuses
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 开始删除非白名单补全草稿: "
|
||||||
|
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}, "
|
||||||
|
f"delete_conditions={self._format_delete_conditions(effective_conditions)}"
|
||||||
|
)
|
||||||
|
initial_tags = self.get_complete_draft_quick_view_tags(shop_name, country)
|
||||||
|
delete_candidates = self._build_deletable_complete_draft_tag_rows(initial_tags, effective_conditions)
|
||||||
|
results: List[Dict[str, Any]] = []
|
||||||
|
delete_counts: Dict[str, int] = {}
|
||||||
|
total_deleted = 0
|
||||||
|
stop_reason = "no-candidates"
|
||||||
|
|
||||||
|
if not delete_candidates:
|
||||||
|
logger.info(f"trace_id={trace_id} 补全草稿没有可删除标签: shop={shop_name}, country={country}")
|
||||||
|
return {
|
||||||
|
"shopName": shop_name,
|
||||||
|
"country": country,
|
||||||
|
"draftRows": initial_tags,
|
||||||
|
"deleteCandidates": delete_candidates,
|
||||||
|
"results": results,
|
||||||
|
"deleteCounts": delete_counts,
|
||||||
|
"totalDeleted": total_deleted,
|
||||||
|
"stopReason": stop_reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_reason = "processed-candidates"
|
||||||
|
for candidate in delete_candidates:
|
||||||
|
tag = candidate["tag"]
|
||||||
|
quantity = candidate["quantity"]
|
||||||
|
canonical_tag = candidate["canonicalTag"]
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 开始删除补全草稿标签下方商品: "
|
||||||
|
f"shop={shop_name}, country={country}, tag={tag}, quantity={quantity}"
|
||||||
|
)
|
||||||
|
self.select_complete_draft_tag(tag, trace_id=trace_id)
|
||||||
|
deleted_for_tag = 0
|
||||||
|
|
||||||
|
while quantity <= 0 or deleted_for_tag < quantity:
|
||||||
|
remaining_limit = None
|
||||||
|
if max_delete_operations is not None:
|
||||||
|
remaining_limit = max(0, max_delete_operations - total_deleted)
|
||||||
|
if remaining_limit <= 0:
|
||||||
|
stop_reason = "max-delete-operations"
|
||||||
|
break
|
||||||
|
select_result = self._select_all_complete_draft_rows(
|
||||||
|
tag,
|
||||||
|
trace_id=trace_id,
|
||||||
|
max_select_count=remaining_limit,
|
||||||
|
)
|
||||||
|
selected_count = int(select_result.get("selectedCount") or select_result.get("rowCount") or 0)
|
||||||
|
if select_result.get("empty"):
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿标签下方已无商品,跳过删除: "
|
||||||
|
f"shop={shop_name}, country={country}, tag={tag}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
if selected_count <= 0:
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿标签下方没有可删除商品: "
|
||||||
|
f"shop={shop_name}, country={country}, tag={tag}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
delete_result = self._click_complete_draft_delete_selected(tag, trace_id=trace_id)
|
||||||
|
self._confirm_delete_modal(tag, trace_id=trace_id)
|
||||||
|
self._wait_complete_draft_rows_after_delete(tag, select_result, trace_id=trace_id)
|
||||||
|
deleted_count = min(selected_count, quantity - deleted_for_tag) if quantity > 0 else selected_count
|
||||||
|
deleted_for_tag += deleted_count
|
||||||
|
total_deleted += deleted_count
|
||||||
|
delete_counts[canonical_tag] = delete_counts.get(canonical_tag, 0) + deleted_count
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"tag": tag,
|
||||||
|
"canonicalTag": canonical_tag,
|
||||||
|
"tagQuantity": quantity,
|
||||||
|
"deletedCount": deleted_count,
|
||||||
|
"selectResult": select_result,
|
||||||
|
"deleteResult": delete_result,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿标签删除成功: "
|
||||||
|
f"shop={shop_name}, country={country}, tag={tag}, "
|
||||||
|
f"deleted_count={deleted_count}, tag_deleted={deleted_for_tag}, total_deleted={total_deleted}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if max_delete_operations is not None and total_deleted >= max_delete_operations:
|
||||||
|
stop_reason = "max-delete-operations"
|
||||||
|
break
|
||||||
|
if quantity > 0 and deleted_for_tag >= quantity:
|
||||||
|
break
|
||||||
|
|
||||||
|
if stop_reason == "max-delete-operations":
|
||||||
|
break
|
||||||
|
|
||||||
|
final_tags = self.get_complete_draft_quick_view_tags(shop_name, country)
|
||||||
|
return {
|
||||||
|
"shopName": shop_name,
|
||||||
|
"country": country,
|
||||||
|
"draftRows": final_tags,
|
||||||
|
"deleteCandidates": delete_candidates,
|
||||||
|
"results": results,
|
||||||
|
"deleteCounts": delete_counts,
|
||||||
|
"totalDeleted": total_deleted,
|
||||||
|
"stopReason": stop_reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
def select_complete_draft_tag(self, tag: str, trace_id: str = "") -> Dict[str, Any]:
|
||||||
|
normalized_tag = self._normalize_option_text(tag)
|
||||||
|
if not normalized_tag:
|
||||||
|
raise RuntimeError("补全草稿标签为空,无法执行筛选")
|
||||||
|
trace_id = trace_id or _new_trace_id("draft")
|
||||||
|
result = self._run_js(COMPLETE_DRAFT_SELECT_TAG_SCRIPT, {"tag": normalized_tag}) or {}
|
||||||
|
if not result.get("ok"):
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿快速查看标签未命中: "
|
||||||
|
f"tag={normalized_tag}, result={_safe_log_text(result)}"
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"未找到补全草稿标签[{normalized_tag}]: {result}")
|
||||||
|
time.sleep(LISTING_STATUS_SELECT_DELAY)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _select_all_complete_draft_rows(
|
||||||
|
self,
|
||||||
|
tag: str,
|
||||||
|
trace_id: str = "",
|
||||||
|
max_select_count: Optional[int] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
trace_id = trace_id or _new_trace_id("draft")
|
||||||
|
deadline = time.time() + INVENTORY_ROW_TIMEOUT
|
||||||
|
result: Dict[str, Any] = {}
|
||||||
|
saw_empty = False
|
||||||
|
while time.time() < deadline:
|
||||||
|
payload = {"maxSelectCount": max_select_count} if max_select_count is not None else None
|
||||||
|
result = self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT, payload) if payload else self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT)
|
||||||
|
result = result or {}
|
||||||
|
if result.get("ok"):
|
||||||
|
time.sleep(0.5)
|
||||||
|
logger.info(f"trace_id={trace_id} 补全草稿已全选: tag={tag}, result={_safe_log_text(result)}")
|
||||||
|
return result
|
||||||
|
if result.get("reason") == "no complete draft rows":
|
||||||
|
saw_empty = True
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
if result.get("reason") and result.get("reason") != "no complete draft rows":
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if not result.get("ok"):
|
||||||
|
if saw_empty and result.get("reason") == "no complete draft rows":
|
||||||
|
logger.info(f"trace_id={trace_id} 补全草稿标签下方无商品行: tag={tag}, result={_safe_log_text(result)}")
|
||||||
|
return {**result, "ok": True, "empty": True, "selectedCount": 0, "rowCount": 0}
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿全选失败: tag={tag}, result={_safe_log_text(result)}"
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"补全草稿标签[{tag}]全选失败: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _click_complete_draft_delete_selected(self, tag: str, trace_id: str = "") -> Dict[str, Any]:
|
||||||
|
trace_id = trace_id or _new_trace_id("draft")
|
||||||
|
result: Dict[str, Any] = {}
|
||||||
|
for attempt in range(8):
|
||||||
|
result = self._run_js(COMPLETE_DRAFT_DELETE_SELECTED_SCRIPT) or {}
|
||||||
|
if result.get("ok"):
|
||||||
|
break
|
||||||
|
if result.get("menuOpened"):
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿选择组操作菜单已打开,等待删除选项出现: "
|
||||||
|
f"tag={tag}, attempt={attempt + 1}, result={_safe_log_text(result)}"
|
||||||
|
)
|
||||||
|
time.sleep(0.8)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if not result.get("ok"):
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿点击删除失败: tag={tag}, result={_safe_log_text(result)}"
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"补全草稿标签[{tag}]点击删除失败: {result}")
|
||||||
|
time.sleep(0.5)
|
||||||
|
logger.info(f"trace_id={trace_id} 补全草稿删除按钮点击成功: tag={tag}, result={_safe_log_text(result)}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _wait_complete_draft_rows_after_delete(
|
||||||
|
self,
|
||||||
|
tag: str,
|
||||||
|
before_result: Dict[str, Any],
|
||||||
|
trace_id: str = "",
|
||||||
|
) -> None:
|
||||||
|
trace_id = trace_id or _new_trace_id("draft")
|
||||||
|
before_count = int(before_result.get("rowCount") or 0)
|
||||||
|
deadline = time.time() + DELETE_SUCCESS_TIMEOUT
|
||||||
|
last_result: Dict[str, Any] = {}
|
||||||
|
while time.time() < deadline:
|
||||||
|
time.sleep(0.5)
|
||||||
|
result = self._run_js(COMPLETE_DRAFT_SELECT_ALL_SCRIPT, {"probeOnly": True}) or {}
|
||||||
|
last_result = result
|
||||||
|
row_count = int(result.get("rowCount") or 0)
|
||||||
|
if row_count < before_count or row_count == 0:
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿删除后行数已变化: "
|
||||||
|
f"tag={tag}, before={before_count}, after={row_count}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
for text in self._collect_delete_success_messages():
|
||||||
|
if self._is_complete_draft_delete_success_message(text) or self._is_delete_success_message(text):
|
||||||
|
logger.info(f"trace_id={trace_id} 补全草稿删除读取到成功提示: tag={tag}, message={_safe_log_text(text)}")
|
||||||
|
return
|
||||||
|
logger.info(
|
||||||
|
f"trace_id={trace_id} 补全草稿删除后未确认行数变化: "
|
||||||
|
f"tag={tag}, before={before_count}, last_result={_safe_log_text(last_result)}"
|
||||||
|
)
|
||||||
|
raise RuntimeError("补全草稿删除后未确认行数变化")
|
||||||
|
|
||||||
def open_recommended_offer_percentage_detail(self) -> Dict[str, Any]:
|
def open_recommended_offer_percentage_detail(self) -> Dict[str, Any]:
|
||||||
"""打开 Seller Central 首页中的推荐报价百分比明细。"""
|
"""打开 Seller Central 首页中的推荐报价百分比明细。"""
|
||||||
if self.tab is None:
|
if self.tab is None:
|
||||||
@@ -234,14 +466,16 @@ class InventoryManage(AmamzonBase):
|
|||||||
country: str,
|
country: str,
|
||||||
max_delete_operations: Optional[int] = None,
|
max_delete_operations: Optional[int] = None,
|
||||||
trace_id: str = "",
|
trace_id: str = "",
|
||||||
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
delete_conditions: Optional[Iterable[Any]] = None,
|
||||||
|
allowed_statuses: Optional[Iterable[str]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
|
"""循环删除非白名单分类商品,直到当前国家没有可删项。"""
|
||||||
trace_id = trace_id or _new_trace_id("del")
|
trace_id = trace_id or _new_trace_id("del")
|
||||||
|
effective_conditions = delete_conditions if delete_conditions is not None else allowed_statuses
|
||||||
logger.info(
|
logger.info(
|
||||||
f"trace_id={trace_id} 开始批量删除非白名单商品: "
|
f"trace_id={trace_id} 开始批量删除非白名单商品: "
|
||||||
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}, "
|
f"shop={shop_name}, country={country}, max_delete_operations={max_delete_operations}, "
|
||||||
f"delete_conditions={self._format_delete_conditions(delete_conditions)}"
|
f"delete_conditions={self._format_delete_conditions(effective_conditions)}"
|
||||||
)
|
)
|
||||||
all_results: List[Dict[str, Any]] = []
|
all_results: List[Dict[str, Any]] = []
|
||||||
delete_counts: Dict[str, int] = {}
|
delete_counts: Dict[str, int] = {}
|
||||||
@@ -253,7 +487,7 @@ class InventoryManage(AmamzonBase):
|
|||||||
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country, trace_id=trace_id)
|
latest_status_rows = self._read_listing_status_rows_for_delete(shop_name, country, trace_id=trace_id)
|
||||||
latest_delete_candidates = self._build_deletable_status_rows(
|
latest_delete_candidates = self._build_deletable_status_rows(
|
||||||
latest_status_rows,
|
latest_status_rows,
|
||||||
delete_conditions=delete_conditions,
|
delete_conditions=effective_conditions,
|
||||||
)
|
)
|
||||||
if not latest_delete_candidates:
|
if not latest_delete_candidates:
|
||||||
logger.info(f"trace_id={trace_id} 未找到可删除商品分类: shop={shop_name}, country={country}")
|
logger.info(f"trace_id={trace_id} 未找到可删除商品分类: shop={shop_name}, country={country}")
|
||||||
@@ -595,6 +829,11 @@ class InventoryManage(AmamzonBase):
|
|||||||
return True
|
return True
|
||||||
return any(pattern.search(normalized) for pattern in DELETE_SUCCESS_TEXT_PATTERNS)
|
return any(pattern.search(normalized) for pattern in DELETE_SUCCESS_TEXT_PATTERNS)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_complete_draft_delete_success_message(text: str) -> bool:
|
||||||
|
normalized = re.sub(r"\s+", "", text or "")
|
||||||
|
return any(pattern.search(normalized) for pattern in COMPLETE_DRAFT_DELETE_SUCCESS_PATTERNS)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_delete_success_count(text: str) -> int:
|
def _extract_delete_success_count(text: str) -> int:
|
||||||
normalized = re.sub(r"\s+", "", text or "")
|
normalized = re.sub(r"\s+", "", text or "")
|
||||||
@@ -791,9 +1030,15 @@ class InventoryManage(AmamzonBase):
|
|||||||
def _build_deletable_status_rows(
|
def _build_deletable_status_rows(
|
||||||
cls,
|
cls,
|
||||||
rows: Iterable[Dict[str, Any]],
|
rows: Iterable[Dict[str, Any]],
|
||||||
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
delete_conditions: Optional[Iterable[Any]] = None,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
condition_texts = cls._normalize_delete_condition_texts(delete_conditions)
|
condition_texts = cls._normalize_delete_condition_texts(delete_conditions)
|
||||||
|
if not condition_texts:
|
||||||
|
return []
|
||||||
|
inventory_status_enabled = cls._matches_any_delete_condition(
|
||||||
|
("库存状态删除", "商品状态删除", "库存状态"),
|
||||||
|
condition_texts,
|
||||||
|
)
|
||||||
candidates = []
|
candidates = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
status = cls._normalize_option_text(row.get("status") or "")
|
status = cls._normalize_option_text(row.get("status") or "")
|
||||||
@@ -805,7 +1050,7 @@ class InventoryManage(AmamzonBase):
|
|||||||
canonical_status = cls._canonical_listing_status(status)
|
canonical_status = cls._canonical_listing_status(status)
|
||||||
if not quantity_text or quantity <= 0 or canonical_status in LISTING_STATUS_DELETE_WHITELIST:
|
if not quantity_text or quantity <= 0 or canonical_status in LISTING_STATUS_DELETE_WHITELIST:
|
||||||
continue
|
continue
|
||||||
if condition_texts and not cls._matches_delete_conditions(status, condition_texts):
|
if not inventory_status_enabled and not cls._matches_delete_conditions(status, condition_texts):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
candidates.append(
|
candidates.append(
|
||||||
@@ -820,17 +1065,29 @@ class InventoryManage(AmamzonBase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _normalize_delete_condition_texts(
|
def _normalize_delete_condition_texts(
|
||||||
cls,
|
cls,
|
||||||
delete_conditions: Optional[List[Dict[str, Any]]],
|
delete_conditions: Optional[Iterable[Any]],
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
texts = []
|
texts = []
|
||||||
|
if delete_conditions is None:
|
||||||
|
return texts
|
||||||
|
if isinstance(delete_conditions, (str, bytes, dict)):
|
||||||
|
delete_conditions = [delete_conditions]
|
||||||
for condition in delete_conditions or []:
|
for condition in delete_conditions or []:
|
||||||
if isinstance(condition, dict):
|
if isinstance(condition, dict):
|
||||||
raw_text = condition.get("conditionText") or condition.get("condition_text") or condition.get("text")
|
raw_text = next(
|
||||||
|
(
|
||||||
|
condition.get(key)
|
||||||
|
for key in ("conditionText", "condition_text", "status", "text", "label", "name")
|
||||||
|
if condition.get(key)
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raw_text = condition
|
raw_text = condition
|
||||||
text = cls._normalize_option_text(str(raw_text or "")).lower()
|
for part in re.split(r"[,,;;、\r\n]+", str(raw_text or "")):
|
||||||
if text:
|
text = cls._normalize_option_text(part).lower()
|
||||||
texts.append(text)
|
if text:
|
||||||
|
texts.append(text)
|
||||||
return list(dict.fromkeys(texts))
|
return list(dict.fromkeys(texts))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -846,9 +1103,49 @@ class InventoryManage(AmamzonBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_delete_conditions(cls, delete_conditions: Optional[List[Dict[str, Any]]]) -> str:
|
def _matches_any_delete_condition(cls, statuses: Iterable[str], condition_texts: List[str]) -> bool:
|
||||||
|
return any(cls._matches_delete_conditions(status, condition_texts) for status in statuses)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_delete_conditions(cls, delete_conditions: Optional[Iterable[Any]]) -> str:
|
||||||
texts = cls._normalize_delete_condition_texts(delete_conditions)
|
texts = cls._normalize_delete_condition_texts(delete_conditions)
|
||||||
return ", ".join(texts) if texts else "default"
|
return ", ".join(texts) if texts else "未指定"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_deletable_complete_draft_tag_rows(
|
||||||
|
cls,
|
||||||
|
rows: Iterable[Dict[str, Any]],
|
||||||
|
delete_conditions: Optional[Iterable[Any]] = None,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
condition_texts = cls._normalize_delete_condition_texts(delete_conditions)
|
||||||
|
if not condition_texts:
|
||||||
|
return []
|
||||||
|
complete_draft_enabled = bool(
|
||||||
|
cls._matches_any_delete_condition(
|
||||||
|
("补全草稿删除", "补全草稿", "商品信息草稿"),
|
||||||
|
condition_texts,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
candidates = []
|
||||||
|
for row in rows:
|
||||||
|
tag = cls._normalize_option_text(row.get("tag") or row.get("status") or "")
|
||||||
|
if not tag:
|
||||||
|
continue
|
||||||
|
quantity_text = cls._normalize_quantity_text(row.get("quantity"))
|
||||||
|
quantity = int(quantity_text) if quantity_text else 0
|
||||||
|
canonical_tag = cls._canonical_listing_status(tag)
|
||||||
|
if not quantity_text or quantity <= 0 or canonical_tag in LISTING_STATUS_DELETE_WHITELIST:
|
||||||
|
continue
|
||||||
|
if not complete_draft_enabled and not cls._matches_delete_conditions(tag, condition_texts):
|
||||||
|
continue
|
||||||
|
candidates.append(
|
||||||
|
{
|
||||||
|
"tag": tag,
|
||||||
|
"canonicalTag": canonical_tag,
|
||||||
|
"quantity": quantity,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return candidates
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_deletable_candidates(candidates: Iterable[Dict[str, Any]]) -> str:
|
def _format_deletable_candidates(candidates: Iterable[Dict[str, Any]]) -> str:
|
||||||
@@ -864,6 +1161,21 @@ class InventoryManage(AmamzonBase):
|
|||||||
normalized = cls._normalize_option_text(status)
|
normalized = cls._normalize_option_text(status)
|
||||||
return LISTING_STATUS_COMPATIBILITY_MAP.get(normalized, normalized)
|
return LISTING_STATUS_COMPATIBILITY_MAP.get(normalized, normalized)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_allowed_status_set(cls, statuses: Optional[Iterable[str]]) -> Optional[Set[str]]:
|
||||||
|
if statuses is None:
|
||||||
|
return None
|
||||||
|
allowed = set()
|
||||||
|
for status in statuses:
|
||||||
|
text = cls._normalize_option_text(str(status or ""))
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
for part in re.split(r"[,,;;、\r\n]+", text):
|
||||||
|
normalized = cls._normalize_option_text(part)
|
||||||
|
if normalized:
|
||||||
|
allowed.add(cls._canonical_listing_status(normalized))
|
||||||
|
return allowed
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_option_text(text: str) -> str:
|
def _normalize_option_text(text: str) -> str:
|
||||||
return re.sub(r"\s+", " ", text).strip()
|
return re.sub(r"\s+", " ", text).strip()
|
||||||
@@ -1344,7 +1656,7 @@ class PatrolDeleteTask:
|
|||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
country_sections = data.get("country_sections", [])
|
country_sections = data.get("country_sections", [])
|
||||||
cart_ratios = data.get("cart_ratios", [])
|
cart_ratios = data.get("cart_ratios", [])
|
||||||
delete_conditions = data.get("delete_conditions") or data.get("deleteConditions") or []
|
delete_conditions = self._resolve_delete_condition_texts(data)
|
||||||
|
|
||||||
if not task_id:
|
if not task_id:
|
||||||
self.log("任务ID为空,跳过", "WARNING")
|
self.log("任务ID为空,跳过", "WARNING")
|
||||||
@@ -1373,7 +1685,7 @@ class PatrolDeleteTask:
|
|||||||
|
|
||||||
self.log(
|
self.log(
|
||||||
f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家,"
|
f"开始处理巡店删除任务 {task_id},共 {len(items)} 个店铺,{total_countries} 个国家,"
|
||||||
f"删除条件 {len(delete_conditions)} 条"
|
f"删除条件={delete_conditions if delete_conditions is not None else '未指定'}"
|
||||||
)
|
)
|
||||||
for index, shop_item in enumerate(items, 1):
|
for index, shop_item in enumerate(items, 1):
|
||||||
if runing_task[task_id].get("stop_requested", False):
|
if runing_task[task_id].get("stop_requested", False):
|
||||||
@@ -1409,7 +1721,7 @@ class PatrolDeleteTask:
|
|||||||
shop_item: Dict[str, Any],
|
shop_item: Dict[str, Any],
|
||||||
country_sections: List[Dict[str, Any]],
|
country_sections: List[Dict[str, Any]],
|
||||||
cart_ratios: List[Dict[str, Any]],
|
cart_ratios: List[Dict[str, Any]],
|
||||||
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
delete_conditions: Optional[List[str]] = None,
|
||||||
):
|
):
|
||||||
"""处理单个店铺。"""
|
"""处理单个店铺。"""
|
||||||
from config import runing_shop, runing_task
|
from config import runing_shop, runing_task
|
||||||
@@ -1435,7 +1747,10 @@ class PatrolDeleteTask:
|
|||||||
country_names = self._ordered_countries(payload_country_sections)
|
country_names = self._ordered_countries(payload_country_sections)
|
||||||
aggregated_sections = self._build_country_section_templates(payload_country_sections)
|
aggregated_sections = self._build_country_section_templates(payload_country_sections)
|
||||||
aggregated_cart_ratios = self._build_cart_ratio_templates(country_names, payload_cart_ratios)
|
aggregated_cart_ratios = self._build_cart_ratio_templates(country_names, payload_cart_ratios)
|
||||||
self.log(f"店铺 {shop_name} 待处理国家: {', '.join(country_names)}")
|
self.log(
|
||||||
|
f"店铺 {shop_name} 待处理国家: {', '.join(country_names)},"
|
||||||
|
f"删除条件={delete_conditions if delete_conditions is not None else '未指定'}"
|
||||||
|
)
|
||||||
|
|
||||||
driver: Optional[InventoryManage] = None
|
driver: Optional[InventoryManage] = None
|
||||||
requires_browser_reset = False
|
requires_browser_reset = False
|
||||||
@@ -1477,6 +1792,10 @@ class PatrolDeleteTask:
|
|||||||
f"国家 {country_name} 处理结束,success={country_result['success']},"
|
f"国家 {country_name} 处理结束,success={country_result['success']},"
|
||||||
f"deleted={country_result['deletedCount']},reopenRequired={requires_browser_reset}"
|
f"deleted={country_result['deletedCount']},reopenRequired={requires_browser_reset}"
|
||||||
)
|
)
|
||||||
|
self._post_progress_result_safely(
|
||||||
|
task_id=task_id,
|
||||||
|
shop_name=shop_name,
|
||||||
|
)
|
||||||
|
|
||||||
if driver is not None:
|
if driver is not None:
|
||||||
self.log(f"店铺 {shop_name} 国家处理完成,正在关闭店铺")
|
self.log(f"店铺 {shop_name} 国家处理完成,正在关闭店铺")
|
||||||
@@ -1507,6 +1826,22 @@ class PatrolDeleteTask:
|
|||||||
del runing_shop[shop_name]
|
del runing_shop[shop_name]
|
||||||
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
||||||
|
|
||||||
|
def _post_progress_result_safely(
|
||||||
|
self,
|
||||||
|
task_id: int,
|
||||||
|
shop_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Submit a heartbeat-only payload so Java does not append duplicate result rows."""
|
||||||
|
try:
|
||||||
|
self.post_result(
|
||||||
|
task_id=task_id,
|
||||||
|
shop_name=shop_name,
|
||||||
|
shop_done=False,
|
||||||
|
heartbeat_only=True,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self.log(f"店铺 {shop_name} 中间结果回传失败,继续执行: {str(exc)}", "WARNING")
|
||||||
|
|
||||||
def open_shop(self, max_retries: int, company_name: str, shop_name: str, iskill: bool = False) -> Optional[InventoryManage]:
|
def open_shop(self, max_retries: int, company_name: str, shop_name: str, iskill: bool = False) -> Optional[InventoryManage]:
|
||||||
error_info = ""
|
error_info = ""
|
||||||
driver: Optional[InventoryManage] = None
|
driver: Optional[InventoryManage] = None
|
||||||
@@ -1574,7 +1909,7 @@ class PatrolDeleteTask:
|
|||||||
task_id: int,
|
task_id: int,
|
||||||
shop_name: str,
|
shop_name: str,
|
||||||
country_name: str,
|
country_name: str,
|
||||||
delete_conditions: Optional[List[Dict[str, Any]]] = None,
|
delete_conditions: Optional[List[str]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
from config import runing_task
|
from config import runing_task
|
||||||
|
|
||||||
@@ -1609,17 +1944,26 @@ class PatrolDeleteTask:
|
|||||||
delete_counts=delete_result.get("deleteCounts") or {},
|
delete_counts=delete_result.get("deleteCounts") or {},
|
||||||
default_process_status="无可删商品" if delete_result.get("totalDeleted", 0) == 0 else "",
|
default_process_status="无可删商品" if delete_result.get("totalDeleted", 0) == 0 else "",
|
||||||
)
|
)
|
||||||
complete_draft_section = self._read_complete_draft_country_section_safe(driver, shop_name, country_name)
|
complete_draft_delete_result = self._delete_complete_draft_country_section_safe(
|
||||||
|
driver,
|
||||||
|
shop_name,
|
||||||
|
country_name,
|
||||||
|
delete_conditions,
|
||||||
|
)
|
||||||
|
complete_draft_section = complete_draft_delete_result.get("countrySection")
|
||||||
country_section = self._replace_complete_draft_rows(country_section, complete_draft_section)
|
country_section = self._replace_complete_draft_rows(country_section, complete_draft_section)
|
||||||
|
|
||||||
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
|
cart_ratio = self._read_country_cart_ratio_safe(driver, country_name)
|
||||||
self.log(f"trace_id={trace_id} 国家 {country_name} 购物车比例读取完成: {cart_ratio.get('ratio', '')}")
|
self.log(f"trace_id={trace_id} 国家 {country_name} 购物车比例读取完成: {cart_ratio.get('ratio', '')}")
|
||||||
|
|
||||||
|
total_deleted = int(delete_result.get("totalDeleted") or 0) + int(
|
||||||
|
complete_draft_delete_result.get("deletedCount") or 0
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"countrySection": country_section,
|
"countrySection": country_section,
|
||||||
"cartRatio": cart_ratio,
|
"cartRatio": cart_ratio,
|
||||||
"deletedCount": delete_result.get("totalDeleted", 0),
|
"deletedCount": total_deleted,
|
||||||
"reopenRequired": False,
|
"reopenRequired": False,
|
||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -1672,6 +2016,8 @@ class PatrolDeleteTask:
|
|||||||
"type": "completeDraft",
|
"type": "completeDraft",
|
||||||
"status": PatrolDeleteTask._string_result_field(item.get("tag")),
|
"status": PatrolDeleteTask._string_result_field(item.get("tag")),
|
||||||
"quantity": quantity,
|
"quantity": quantity,
|
||||||
|
"deleteQuantity": "",
|
||||||
|
"processStatus": "",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.log(f"trace_id={trace_id} 国家 {country_name} 补全草稿读取完成: rows={rows}")
|
self.log(f"trace_id={trace_id} 国家 {country_name} 补全草稿读取完成: rows={rows}")
|
||||||
@@ -1680,6 +2026,52 @@ class PatrolDeleteTask:
|
|||||||
self.log(f"trace_id={trace_id} 读取国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
|
self.log(f"trace_id={trace_id} 读取国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _delete_complete_draft_country_section_safe(
|
||||||
|
self,
|
||||||
|
driver: InventoryManage,
|
||||||
|
shop_name: str,
|
||||||
|
country_name: str,
|
||||||
|
delete_conditions: Optional[List[str]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
trace_id = _new_trace_id("draft")
|
||||||
|
try:
|
||||||
|
self.log(f"trace_id={trace_id} 开始处理国家 {country_name} 补全草稿删除")
|
||||||
|
delete_result = driver.delete_complete_drafts_from_non_whitelisted_tags(
|
||||||
|
shop_name,
|
||||||
|
country_name,
|
||||||
|
trace_id=trace_id,
|
||||||
|
delete_conditions=delete_conditions,
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
delete_counts = delete_result.get("deleteCounts") or {}
|
||||||
|
for item in delete_result.get("draftRows") or []:
|
||||||
|
status = PatrolDeleteTask._string_result_field(item.get("tag") or item.get("status"))
|
||||||
|
canonical_status = InventoryManage._canonical_listing_status(status)
|
||||||
|
deleted_count = int(delete_counts.get(canonical_status) or 0)
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"type": "completeDraft",
|
||||||
|
"status": status,
|
||||||
|
"quantity": InventoryManage._normalize_quantity_text(item.get("quantity")),
|
||||||
|
"deleteQuantity": str(deleted_count),
|
||||||
|
"processStatus": "已完成" if deleted_count > 0 else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
section = {"country": country_name, "rows": rows} if rows else None
|
||||||
|
self.log(
|
||||||
|
f"trace_id={trace_id} 国家 {country_name} 补全草稿删除处理完成: "
|
||||||
|
f"deleted={delete_result.get('totalDeleted', 0)}, rows={rows}"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"countrySection": section,
|
||||||
|
"deletedCount": int(delete_result.get("totalDeleted") or 0),
|
||||||
|
"deleteResult": delete_result,
|
||||||
|
}
|
||||||
|
except Exception as draft_exc:
|
||||||
|
self.log(f"trace_id={trace_id} 删除国家 {country_name} 补全草稿失败: {str(draft_exc)}", "WARNING")
|
||||||
|
fallback_section = self._read_complete_draft_country_section_safe(driver, shop_name, country_name)
|
||||||
|
return {"countrySection": fallback_section, "deletedCount": 0, "error": str(draft_exc)}
|
||||||
|
|
||||||
def _switch_country_with_retry(self, driver: InventoryManage, country_name: str) -> None:
|
def _switch_country_with_retry(self, driver: InventoryManage, country_name: str) -> None:
|
||||||
trace_id = _new_trace_id("switch")
|
trace_id = _new_trace_id("switch")
|
||||||
for retry in range(PRODUCT_TASK_MAX_RETRIES):
|
for retry in range(PRODUCT_TASK_MAX_RETRIES):
|
||||||
@@ -1704,6 +2096,41 @@ class PatrolDeleteTask:
|
|||||||
time.sleep(PRODUCT_TASK_RETRY_DELAY)
|
time.sleep(PRODUCT_TASK_RETRY_DELAY)
|
||||||
raise RuntimeError(f"切换国家 {country_name} 失败")
|
raise RuntimeError(f"切换国家 {country_name} 失败")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_delete_condition_texts(cls, data: Dict[str, Any]) -> Optional[List[str]]:
|
||||||
|
if "delete_conditions" in data:
|
||||||
|
raw_conditions = data.get("delete_conditions")
|
||||||
|
elif "deleteConditions" in data:
|
||||||
|
raw_conditions = data.get("deleteConditions")
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
return cls._parse_delete_condition_texts(raw_conditions)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_delete_condition_texts(cls, raw_conditions: Any) -> List[str]:
|
||||||
|
if raw_conditions is None:
|
||||||
|
return []
|
||||||
|
if not isinstance(raw_conditions, list):
|
||||||
|
raw_conditions = [raw_conditions]
|
||||||
|
|
||||||
|
texts: List[str] = []
|
||||||
|
for item in raw_conditions:
|
||||||
|
text = ""
|
||||||
|
if isinstance(item, dict):
|
||||||
|
for key in ("conditionText", "condition_text", "status", "text", "label", "name"):
|
||||||
|
value = item.get(key)
|
||||||
|
if value:
|
||||||
|
text = str(value)
|
||||||
|
break
|
||||||
|
elif item is not None:
|
||||||
|
text = str(item)
|
||||||
|
|
||||||
|
for part in re.split(r"[,,;;、\r\n]+", text):
|
||||||
|
normalized = InventoryManage._normalize_option_text(part)
|
||||||
|
if normalized and normalized not in texts:
|
||||||
|
texts.append(normalized)
|
||||||
|
return texts
|
||||||
|
|
||||||
def post_result(
|
def post_result(
|
||||||
self,
|
self,
|
||||||
task_id: int,
|
task_id: int,
|
||||||
@@ -1712,6 +2139,7 @@ class PatrolDeleteTask:
|
|||||||
cart_ratios: Optional[List[Dict[str, Any]]] = None,
|
cart_ratios: Optional[List[Dict[str, Any]]] = None,
|
||||||
error: str = "",
|
error: str = "",
|
||||||
shop_done: bool = False,
|
shop_done: bool = False,
|
||||||
|
heartbeat_only: bool = False,
|
||||||
):
|
):
|
||||||
"""回传巡店删除结果到 Java API。"""
|
"""回传巡店删除结果到 Java API。"""
|
||||||
from config import DELETE_BRAND_API_BASE
|
from config import DELETE_BRAND_API_BASE
|
||||||
@@ -1726,6 +2154,8 @@ class PatrolDeleteTask:
|
|||||||
}
|
}
|
||||||
if error:
|
if error:
|
||||||
shop_payload["error"] = error
|
shop_payload["error"] = error
|
||||||
|
elif heartbeat_only:
|
||||||
|
shop_payload["heartbeatOnly"] = True
|
||||||
else:
|
else:
|
||||||
shop_payload["countrySections"] = self._normalize_country_sections_for_result(country_sections)
|
shop_payload["countrySections"] = self._normalize_country_sections_for_result(country_sections)
|
||||||
shop_payload["cartRatios"] = self._normalize_cart_ratios_for_result(cart_ratios)
|
shop_payload["cartRatios"] = self._normalize_cart_ratios_for_result(cart_ratios)
|
||||||
@@ -1813,9 +2243,12 @@ class PatrolDeleteTask:
|
|||||||
rows = []
|
rows = []
|
||||||
for row in section.get("rows") or []:
|
for row in section.get("rows") or []:
|
||||||
if cls._is_complete_draft_result_row(row):
|
if cls._is_complete_draft_result_row(row):
|
||||||
|
delete_quantity = cls._string_result_field(row.get("deleteQuantity"))
|
||||||
normalized_row = {
|
normalized_row = {
|
||||||
"status": cls._string_result_field(row.get("status")),
|
"status": cls._complete_draft_result_status(row),
|
||||||
"quantity": cls._string_result_field(row.get("quantity")),
|
"quantity": cls._string_result_field(row.get("quantity")),
|
||||||
|
"deleteQuantity": delete_quantity,
|
||||||
|
"processStatus": cls._complete_draft_process_status(row, delete_quantity),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
normalized_row = {
|
normalized_row = {
|
||||||
@@ -1851,6 +2284,19 @@ class PatrolDeleteTask:
|
|||||||
row_type = str(row.get("type") or row.get("kind") or row.get("_kind") or "").strip()
|
row_type = str(row.get("type") or row.get("kind") or row.get("_kind") or "").strip()
|
||||||
return status in {"补全草稿", "商品信息草稿"} or tag == "补全草稿" or row_type == "completeDraft"
|
return status in {"补全草稿", "商品信息草稿"} or tag == "补全草稿" or row_type == "completeDraft"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _complete_draft_result_status(row: Dict[str, Any]) -> str:
|
||||||
|
status = PatrolDeleteTask._string_result_field(row.get("status") or row.get("tag")).strip()
|
||||||
|
return status or "商品信息草稿"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _complete_draft_process_status(row: Dict[str, Any], delete_quantity: str) -> str:
|
||||||
|
process_status = PatrolDeleteTask._string_result_field(row.get("processStatus")).strip()
|
||||||
|
if process_status:
|
||||||
|
return process_status
|
||||||
|
match = re.search(r"\d+", str(delete_quantity or ""))
|
||||||
|
return "已完成" if match and int(match.group(0)) > 0 else ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _replace_complete_draft_rows(
|
def _replace_complete_draft_rows(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
function textOf(node) {
|
||||||
|
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisible(el) {
|
||||||
|
if (!el || !el.getBoundingClientRect) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickElement(el) {
|
||||||
|
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
|
if (el.focus) el.focus();
|
||||||
|
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||||
|
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||||
|
}
|
||||||
|
el.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(el) {
|
||||||
|
return {
|
||||||
|
tag: el.tagName,
|
||||||
|
text: textOf(el).slice(0, 500),
|
||||||
|
attrs: Array.from(el.attributes || []).reduce((acc, attr) => {
|
||||||
|
acc[attr.name] = attr.value;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
outerHTML: (el.outerHTML || '').slice(0, 1000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = document.querySelector('[data-testid="complete-drafts-view"]');
|
||||||
|
if (!view) return { ok: false, reason: 'complete drafts view not found' };
|
||||||
|
|
||||||
|
const selectedRows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||||
|
.filter((row) => isVisible(row) && (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')));
|
||||||
|
if (!selectedRows.length) return { ok: false, reason: 'no selected complete draft rows' };
|
||||||
|
|
||||||
|
const bulkDropdown = view.querySelector('#bulk-dropdown');
|
||||||
|
if (!bulkDropdown) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'bulk dropdown not found',
|
||||||
|
selectedCount: selectedRows.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = bulkDropdown.shadowRoot && bulkDropdown.shadowRoot.querySelector('button[part="dropdown-button-toggle-button"], button');
|
||||||
|
clickElement(toggle || bulkDropdown);
|
||||||
|
|
||||||
|
function findBulkDeleteAction() {
|
||||||
|
const all = [];
|
||||||
|
const seen = new Set();
|
||||||
|
function walk(root) {
|
||||||
|
if (!root || seen.has(root)) return;
|
||||||
|
seen.add(root);
|
||||||
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||||
|
let node = walker.currentNode;
|
||||||
|
while (node) {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) all.push(node);
|
||||||
|
if (node.shadowRoot) walk(node.shadowRoot);
|
||||||
|
if ((node.tagName || '').toLowerCase() === 'slot' && node.assignedElements) {
|
||||||
|
for (const assigned of node.assignedElements({ flatten: true })) walk(assigned);
|
||||||
|
}
|
||||||
|
node = walker.nextNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(document);
|
||||||
|
const exactAction = all.find((el) => {
|
||||||
|
const action = String(el.getAttribute && el.getAttribute('data-action') || '');
|
||||||
|
const text = textOf(el);
|
||||||
|
return isVisible(el) && action === 'DELETE_DRAFTS' && /删除商品信息草稿/.test(text);
|
||||||
|
});
|
||||||
|
if (exactAction) return exactAction;
|
||||||
|
|
||||||
|
return all.find((el) => {
|
||||||
|
const text = textOf(el);
|
||||||
|
const attrs = Array.from(el.attributes || []).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||||
|
const haystack = `${text} ${attrs}`;
|
||||||
|
if (!isVisible(el) || !/删除商品信息草稿/.test(haystack)) return false;
|
||||||
|
if (/^(HTML|BODY|KAT-DATA-GRID)$/i.test(el.tagName || '') && text.length > 80) return false;
|
||||||
|
if (/^DIV$/i.test(el.tagName || '') && text !== '删除商品信息草稿') return false;
|
||||||
|
if ((el.tagName || '').toLowerCase() === 'kat-icon') return false;
|
||||||
|
return /^(BUTTON|KAT-BUTTON|KAT-OPTION|KAT-DROPDOWN-OPTION|LI|A|SPAN|DIV)$/i.test(el.tagName || '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAction = findBulkDeleteAction();
|
||||||
|
|
||||||
|
if (!deleteAction) {
|
||||||
|
const candidates = [];
|
||||||
|
const seen = new Set();
|
||||||
|
function walkCandidates(root) {
|
||||||
|
if (!root || seen.has(root)) return;
|
||||||
|
seen.add(root);
|
||||||
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||||
|
let node = walker.currentNode;
|
||||||
|
while (node) {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE && node.tagName && isVisible(node)) {
|
||||||
|
const text = textOf(node);
|
||||||
|
const attrs = Array.from(node.attributes || []).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||||
|
if (/删除|草稿|选择组操作|menu|option/i.test(`${text} ${attrs}`)) candidates.push(summarize(node));
|
||||||
|
}
|
||||||
|
if (node.shadowRoot) walkCandidates(node.shadowRoot);
|
||||||
|
node = walker.nextNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walkCandidates(document);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'bulk delete draft action not found',
|
||||||
|
menuOpened: true,
|
||||||
|
selectedCount: selectedRows.length,
|
||||||
|
bulkDropdown: summarize(bulkDropdown),
|
||||||
|
candidates: candidates.slice(0, 50),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clickElement(deleteAction);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
selectedCount: selectedRows.length,
|
||||||
|
bulkDropdown: summarize(bulkDropdown),
|
||||||
|
clicked: summarize(deleteAction),
|
||||||
|
rows: selectedRows.slice(0, 10).map(summarize),
|
||||||
|
};
|
||||||
100
app/amazon/scripts/patrol_delete/complete_draft_select_all.js
Normal file
100
app/amazon/scripts/patrol_delete/complete_draft_select_all.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
function textOf(node) {
|
||||||
|
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisible(el) {
|
||||||
|
if (!el || !el.getBoundingClientRect) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickElement(el) {
|
||||||
|
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
|
if (el.focus) el.focus();
|
||||||
|
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||||
|
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||||
|
}
|
||||||
|
el.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeRow(row) {
|
||||||
|
return {
|
||||||
|
rowId: row.getAttribute('row-id') || '',
|
||||||
|
text: textOf(row).slice(0, 500),
|
||||||
|
selected: row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = arguments[0] || {};
|
||||||
|
const view = document.querySelector('[data-testid="complete-drafts-view"]');
|
||||||
|
if (!view) return { ok: false, reason: 'complete drafts view not found' };
|
||||||
|
|
||||||
|
const rows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||||
|
.filter((row) => isVisible(row));
|
||||||
|
const rowCount = rows.length;
|
||||||
|
const selectedBefore = rows.filter((row) => row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')).length;
|
||||||
|
const maxSelectCountRaw = Number(payload.maxSelectCount || 0);
|
||||||
|
const maxSelectCount = Number.isFinite(maxSelectCountRaw) && maxSelectCountRaw > 0 ? Math.floor(maxSelectCountRaw) : 0;
|
||||||
|
|
||||||
|
if (payload.probeOnly) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
probeOnly: true,
|
||||||
|
rowCount,
|
||||||
|
selectedCount: selectedBefore,
|
||||||
|
rows: rows.slice(0, 10).map(summarizeRow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowCount <= 0) {
|
||||||
|
return { ok: false, reason: 'no complete draft rows', rowCount, selectedCount: selectedBefore };
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerCheckbox = view.querySelector('.ag-header-select-all input[type="checkbox"]');
|
||||||
|
if (!headerCheckbox) {
|
||||||
|
return { ok: false, reason: 'complete draft select-all checkbox not found', rowCount, selectedCount: selectedBefore };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxSelectCount > 0 && maxSelectCount < rowCount) {
|
||||||
|
if (headerCheckbox.checked) {
|
||||||
|
clickElement(headerCheckbox);
|
||||||
|
}
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')) {
|
||||||
|
const checkbox = row.querySelector('input[type="checkbox"]');
|
||||||
|
if (checkbox && checkbox.checked) clickElement(checkbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const row of rows.slice(0, maxSelectCount)) {
|
||||||
|
const checkbox = row.querySelector('input[type="checkbox"]');
|
||||||
|
if (checkbox && !checkbox.checked) {
|
||||||
|
clickElement(checkbox);
|
||||||
|
} else if (!checkbox) {
|
||||||
|
clickElement(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!headerCheckbox.checked || selectedBefore < rowCount) {
|
||||||
|
clickElement(headerCheckbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedRows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||||
|
.filter((row) => isVisible(row) && (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')));
|
||||||
|
|
||||||
|
if (selectedRows.length <= 0) {
|
||||||
|
const firstRowCheckbox = rows[0] && rows[0].querySelector('input[type="checkbox"]');
|
||||||
|
if (firstRowCheckbox && !firstRowCheckbox.checked) {
|
||||||
|
clickElement(firstRowCheckbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||||
|
.filter((row) => isVisible(row) && (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')));
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: selectedRows.length > 0,
|
||||||
|
rowCount,
|
||||||
|
selectedCount: selectedRows.length,
|
||||||
|
headerChecked: headerCheckbox.checked,
|
||||||
|
rows: selectedRows.slice(0, 10).map(summarizeRow),
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
function textOf(node) {
|
||||||
|
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisible(el) {
|
||||||
|
if (!el || !el.getBoundingClientRect) return false;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickElement(el) {
|
||||||
|
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
|
if (el.focus) el.focus();
|
||||||
|
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||||
|
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||||
|
}
|
||||||
|
el.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(el) {
|
||||||
|
return {
|
||||||
|
tag: el.tagName,
|
||||||
|
text: textOf(el),
|
||||||
|
className: el.className || '',
|
||||||
|
outerHTML: (el.outerHTML || '').slice(0, 1000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetTag = String((arguments[0] && arguments[0].tag) || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (!targetTag) return { ok: false, reason: 'target tag empty' };
|
||||||
|
|
||||||
|
const view = document.querySelector('[data-testid="complete-drafts-view"]');
|
||||||
|
if (!view) return { ok: false, reason: 'complete drafts view not found' };
|
||||||
|
|
||||||
|
const chips = Array.from(view.querySelectorAll('.JanusChip-module__janusChipContainer--gRjdp'));
|
||||||
|
const chip = chips.find((el) => {
|
||||||
|
const text = textOf(el);
|
||||||
|
return text === targetTag || text.startsWith(`${targetTag} (`) || text.startsWith(`${targetTag}(`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chip) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'complete draft chip not found',
|
||||||
|
tag: targetTag,
|
||||||
|
chips: chips.map(summarize),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/active/.test(String(chip.className || ''))) {
|
||||||
|
clickElement(chip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
tag: targetTag,
|
||||||
|
clicked: summarize(chip),
|
||||||
|
};
|
||||||
1
app/assets/appearance-patent-TI-osB39.css
Normal file
1
app/assets/appearance-patent-TI-osB39.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{aZ as r}from"./pywebview-C66x_2Dh.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
import{aZ as r}from"./pywebview-BWNiqXug.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||||
1
app/assets/categorized-timers-JPA-olTr.js
Normal file
1
app/assets/categorized-timers-JPA-olTr.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
const r=new Map;function c(n){let t=r.get(n);return t||(t=new Map,r.set(n,t)),t}function u(n,t){const e=r.get(n);e&&(e.delete(t),e.size||r.delete(n))}function l(n,t,e){const i=window.setTimeout(()=>{u(n,i),t()},e);return c(n).set(i,{id:i,kind:"timeout",category:n}),i}function a(n,t,e){const i=window.setInterval(t,e);return c(n).set(i,{id:i,kind:"interval",category:n}),i}function f(n,t){if(t==null)return;const i=r.get(n)?.get(t);i?.kind==="interval"?window.clearInterval(t):window.clearTimeout(t),i?.cancel?.(),u(n,t)}function s(n){const t=r.get(n);if(t){for(const e of t.values())e.kind==="interval"?window.clearInterval(e.id):window.clearTimeout(e.id),e.cancel?.();r.delete(n)}}function d(n,t){return new Promise(e=>{const i=window.setTimeout(()=>{u(n,i),e()},t);c(n).set(i,{id:i,kind:"timeout",category:n,cancel:e})})}function m(n){const t=`${n}:`;for(const e of Array.from(r.keys()))(e===n||e.startsWith(t))&&s(e)}function w(n){const t=e=>`${n}:${e}`;return{setTimeout(e,i,o){return l(t(e),i,o)},setInterval(e,i,o){return a(t(e),i,o)},clearTimer(e,i){f(t(e),i)},clearCategory(e){s(t(e))},clearScope(){m(n)},sleep(e,i){return d(t(e),i)}}}export{w as c};
|
||||||
File diff suppressed because one or more lines are too long
1
app/assets/convert-r7MSXq7I.css
Normal file
1
app/assets/convert-r7MSXq7I.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-DMeaPUMZ.css
Normal file
1
app/assets/dedupe-DMeaPUMZ.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-B5ChOXZY.css
Normal file
1
app/assets/delete-brand-B5ChOXZY.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/patrol-delete-DyumfGaZ.css
Normal file
1
app/assets/patrol-delete-DyumfGaZ.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/product-risk-CI6qY5U2.css
Normal file
1
app/assets/product-risk-CI6qY5U2.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/query-asin-Bw6BQsRN.css
Normal file
1
app/assets/query-asin-Bw6BQsRN.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/split-Cw6sXhAg.css
Normal file
1
app/assets/split-Cw6sXhAg.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,13 +7,13 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
from amazon.main import TaskMonitor
|
|
||||||
os.makedirs(cache_path,exist_ok=True)
|
os.makedirs(cache_path,exist_ok=True)
|
||||||
if not debug:
|
if not debug:
|
||||||
today = datetime.datetime.now().strftime("%Y_%m_%d")
|
today = datetime.datetime.now().strftime("%Y_%m_%d")
|
||||||
sys.stdout = open(os.path.join(cache_path,f'{today}.log'), 'a',buffering=1,encoding='utf-8')
|
sys.stdout = open(os.path.join(cache_path,f'{today}.log'), 'a',buffering=1,encoding='utf-8')
|
||||||
sys.stderr = sys.stdout
|
sys.stderr = sys.stdout
|
||||||
|
|
||||||
|
from amazon.main import TaskMonitor
|
||||||
import webview
|
import webview
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>格式转换 - 数富AI</title>
|
<title>格式转换 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
<link rel="stylesheet" crossorigin href="/assets/convert-r7MSXq7I.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据去重 - 数富AI</title>
|
<title>数据去重 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
<link rel="stylesheet" crossorigin href="/assets/dedupe-DMeaPUMZ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>删除品牌 - 数富AI</title>
|
<title>删除品牌 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BfMLVSQU.css">
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-B5ChOXZY.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据拆分 - 数富AI</title>
|
<title>数据拆分 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BtdhG28Q.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-Lp7YWffz.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
<link rel="stylesheet" crossorigin href="/assets/split-Cw6sXhAg.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -185,6 +185,32 @@ def test_wait_until_client_ready_polls_until_available(monkeypatch):
|
|||||||
assert sleeps == [base.CLIENT_READY_INTERVAL, base.CLIENT_READY_INTERVAL]
|
assert sleeps == [base.CLIENT_READY_INTERVAL, base.CLIENT_READY_INTERVAL]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ziniao_webdriver_tail_redacts_password(monkeypatch, tmp_path):
|
||||||
|
appdata = tmp_path / "AppData" / "Roaming"
|
||||||
|
log_dir = appdata / "ziniaobrowser" / "instances" / "userdata1" / "logs" / "client"
|
||||||
|
log_dir.mkdir(parents=True)
|
||||||
|
log_file = log_dir / "webdriver.20260501.log"
|
||||||
|
log_file.write_text(
|
||||||
|
'[error] {"action":"updateCore","username":"u","password":"secret"}\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
monkeypatch.setenv("APPDATA", str(appdata))
|
||||||
|
monkeypatch.setattr(base.time, "strftime", lambda fmt: "20260501")
|
||||||
|
sink_id = base.logger.add(messages.append, level="ERROR", format="{message}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
base.ZiniaoDriver({})._log_ziniao_webdriver_tail()
|
||||||
|
finally:
|
||||||
|
base.logger.remove(sink_id)
|
||||||
|
|
||||||
|
output = "\n".join(messages)
|
||||||
|
assert "webdriver.20260501.log" in output
|
||||||
|
assert '"password":"***"' in output
|
||||||
|
assert "secret" not in output
|
||||||
|
|
||||||
|
|
||||||
def test_close_store_success_uses_current_store(monkeypatch):
|
def test_close_store_success_uses_current_store(monkeypatch):
|
||||||
payloads = []
|
payloads = []
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def test_patrol_delete_task_post_result_retries_business_failure(monkeypatch):
|
|||||||
assert len(calls) == 3
|
assert len(calls) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_country_sections_keeps_only_status_and_quantity_for_complete_draft_rows():
|
def test_normalize_country_sections_keeps_status_fields_for_complete_draft_rows():
|
||||||
rows = PatrolDeleteTask._normalize_country_sections_for_result(
|
rows = PatrolDeleteTask._normalize_country_sections_for_result(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -132,7 +132,7 @@ def test_normalize_country_sections_keeps_only_status_and_quantity_for_complete_
|
|||||||
{
|
{
|
||||||
"country": "意大利",
|
"country": "意大利",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"status": "商品信息草稿", "quantity": "12"},
|
{"status": "商品信息草稿", "quantity": "12", "deleteQuantity": "3", "processStatus": "已完成"},
|
||||||
{"status": "缺少的信息", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"},
|
{"status": "缺少的信息", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -143,15 +143,15 @@ def test_replace_complete_draft_rows_uses_quick_view_rows():
|
|||||||
country_section = {
|
country_section = {
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"},
|
{"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"},
|
||||||
{"status": "商品信息草稿", "quantity": "", "deleteQuantity": "0", "processStatus": ""},
|
{"status": "商品信息草稿", "quantity": "", "deleteQuantity": "0", "processStatus": ""},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
complete_draft_section = {
|
complete_draft_section = {
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"type": "completeDraft", "status": "未提交的草稿", "quantity": "1"},
|
{"type": "completeDraft", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"},
|
||||||
{"type": "completeDraft", "status": "已提交:提供缺少的信息", "quantity": "0"},
|
{"type": "completeDraft", "quantity": "0", "deleteQuantity": "0", "processStatus": ""},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,18 +160,18 @@ def test_replace_complete_draft_rows_uses_quick_view_rows():
|
|||||||
assert merged == {
|
assert merged == {
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"},
|
{"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"},
|
||||||
{"type": "completeDraft", "status": "未提交的草稿", "quantity": "1"},
|
{"type": "completeDraft", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"},
|
||||||
{"type": "completeDraft", "status": "已提交:提供缺少的信息", "quantity": "0"},
|
{"type": "completeDraft", "quantity": "0", "deleteQuantity": "0", "processStatus": ""},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
assert PatrolDeleteTask._normalize_country_sections_for_result([merged]) == [
|
assert PatrolDeleteTask._normalize_country_sections_for_result([merged]) == [
|
||||||
{
|
{
|
||||||
"country": "西班牙",
|
"country": "西班牙",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"status": "全部", "quantity": "6230", "deleteQuantity": "无需删除", "processStatus": "无可删商品"},
|
{"status": "全部", "quantity": "6230", "deleteQuantity": "0", "processStatus": "无可删商品"},
|
||||||
{"status": "未提交的草稿", "quantity": "1"},
|
{"status": "商品信息草稿", "quantity": "1", "deleteQuantity": "1", "processStatus": "已完成"},
|
||||||
{"status": "已提交:提供缺少的信息", "quantity": "0"},
|
{"status": "商品信息草稿", "quantity": "0", "deleteQuantity": "0", "processStatus": ""},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -194,7 +194,7 @@ def test_patrol_delete_task_process_task_aggregates_country_results(monkeypatch)
|
|||||||
def fake_open_shop(self, **kwargs):
|
def fake_open_shop(self, **kwargs):
|
||||||
return Driver()
|
return Driver()
|
||||||
|
|
||||||
def fake_process_country(self, driver, task_id, shop_name, country_name):
|
def fake_process_country(self, driver, task_id, shop_name, country_name, delete_conditions=None):
|
||||||
if country_name == "德国":
|
if country_name == "德国":
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -261,6 +261,18 @@ def test_patrol_delete_task_process_task_aggregates_country_results(monkeypatch)
|
|||||||
assert runing_task[1]["deleted_listings_count"] == 2
|
assert runing_task[1]["deleted_listings_count"] == 2
|
||||||
assert "郭亚芳" not in runing_shop
|
assert "郭亚芳" not in runing_shop
|
||||||
assert calls == [
|
assert calls == [
|
||||||
|
{
|
||||||
|
"task_id": 1,
|
||||||
|
"shop_name": "郭亚芳",
|
||||||
|
"shop_done": False,
|
||||||
|
"heartbeat_only": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"task_id": 1,
|
||||||
|
"shop_name": "郭亚芳",
|
||||||
|
"shop_done": False,
|
||||||
|
"heartbeat_only": True,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"task_id": 1,
|
"task_id": 1,
|
||||||
"shop_name": "郭亚芳",
|
"shop_name": "郭亚芳",
|
||||||
@@ -308,6 +320,102 @@ def test_patrol_delete_task_process_task_skips_missing_required_data(monkeypatch
|
|||||||
assert calls == []
|
assert calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_patrol_delete_task_process_task_passes_delete_conditions(monkeypatch):
|
||||||
|
from config import runing_shop, runing_task
|
||||||
|
|
||||||
|
runing_task.clear()
|
||||||
|
runing_shop.clear()
|
||||||
|
seen_conditions = []
|
||||||
|
|
||||||
|
class Driver:
|
||||||
|
def close_store(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fake_open_shop(self, **kwargs):
|
||||||
|
return Driver()
|
||||||
|
|
||||||
|
def fake_process_country(self, driver, task_id, shop_name, country_name, delete_conditions=None):
|
||||||
|
seen_conditions.append(delete_conditions)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"countrySection": {
|
||||||
|
"country": country_name,
|
||||||
|
"rows": [{"status": "全部", "quantity": "", "deleteQuantity": "0", "processStatus": "无可删商品"}],
|
||||||
|
},
|
||||||
|
"cartRatio": {"country": country_name, "ratio": ""},
|
||||||
|
"deletedCount": 0,
|
||||||
|
"reopenRequired": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(PatrolDeleteTask, "open_shop", fake_open_shop)
|
||||||
|
monkeypatch.setattr(PatrolDeleteTask, "process_country", fake_process_country)
|
||||||
|
monkeypatch.setattr(PatrolDeleteTask, "post_result", lambda self, **kwargs: None)
|
||||||
|
|
||||||
|
PatrolDeleteTask().process_task(
|
||||||
|
{
|
||||||
|
"type": "patrol-delete-run",
|
||||||
|
"data": {
|
||||||
|
"taskId": 7,
|
||||||
|
"items": [{"shopName": "郭亚芳", "companyName": "rongchuang123"}],
|
||||||
|
"country_sections": [{"country": "德国", "rows": [{"status": "全部"}]}],
|
||||||
|
"delete_conditions": [{"id": 1, "conditionText": "商品信息草稿、补全草稿"}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert seen_conditions == [["商品信息草稿", "补全草稿"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_complete_draft_country_section_reports_delete_quantity(monkeypatch):
|
||||||
|
class Driver:
|
||||||
|
def delete_complete_drafts_from_non_whitelisted_tags(self, shop_name, country_name, **kwargs):
|
||||||
|
return {
|
||||||
|
"draftRows": [
|
||||||
|
{"tag": "未提交的草稿", "quantity": 0},
|
||||||
|
{"tag": "已提交:提供缺少的信息", "quantity": 2},
|
||||||
|
],
|
||||||
|
"deleteCounts": {"未提交的草稿": 3},
|
||||||
|
"totalDeleted": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = PatrolDeleteTask()._delete_complete_draft_country_section_safe(
|
||||||
|
Driver(),
|
||||||
|
"郭亚芳",
|
||||||
|
"德国",
|
||||||
|
["未提交的草稿"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["deletedCount"] == 3
|
||||||
|
assert result["countrySection"] == {
|
||||||
|
"country": "德国",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"type": "completeDraft",
|
||||||
|
"status": "未提交的草稿",
|
||||||
|
"quantity": "0",
|
||||||
|
"deleteQuantity": "3",
|
||||||
|
"processStatus": "已完成",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "completeDraft",
|
||||||
|
"status": "已提交:提供缺少的信息",
|
||||||
|
"quantity": "2",
|
||||||
|
"deleteQuantity": "0",
|
||||||
|
"processStatus": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
assert PatrolDeleteTask._normalize_country_sections_for_result([result["countrySection"]]) == [
|
||||||
|
{
|
||||||
|
"country": "德国",
|
||||||
|
"rows": [
|
||||||
|
{"status": "未提交的草稿", "quantity": "0", "deleteQuantity": "3", "processStatus": "已完成"},
|
||||||
|
{"status": "已提交:提供缺少的信息", "quantity": "2", "deleteQuantity": "0", "processStatus": ""},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_patrol_delete_task_reports_shop_error(monkeypatch):
|
def test_patrol_delete_task_reports_shop_error(monkeypatch):
|
||||||
from config import runing_shop, runing_task
|
from config import runing_shop, runing_task
|
||||||
|
|
||||||
@@ -377,7 +485,11 @@ def test_delete_all_listings_from_non_whitelisted_statuses_deletes_each_row_in_s
|
|||||||
monkeypatch.setattr(driver, "_delete_selected_filtered_inventory_rows", fake_delete_selected_filtered_inventory_rows)
|
monkeypatch.setattr(driver, "_delete_selected_filtered_inventory_rows", fake_delete_selected_filtered_inventory_rows)
|
||||||
monkeypatch.setattr(driver, "_wait_inventory_loader", lambda timeout=5: None)
|
monkeypatch.setattr(driver, "_wait_inventory_loader", lambda timeout=5: None)
|
||||||
|
|
||||||
result = driver.delete_all_listings_from_non_whitelisted_statuses("郭亚芳", "德国")
|
result = driver.delete_all_listings_from_non_whitelisted_statuses(
|
||||||
|
"郭亚芳",
|
||||||
|
"德国",
|
||||||
|
delete_conditions=["商品信息草稿"],
|
||||||
|
)
|
||||||
|
|
||||||
assert state["selected"] == ["商品信息草稿"]
|
assert state["selected"] == ["商品信息草稿"]
|
||||||
assert state["deleted"] == 2
|
assert state["deleted"] == 2
|
||||||
@@ -388,6 +500,66 @@ def test_delete_all_listings_from_non_whitelisted_statuses_deletes_each_row_in_s
|
|||||||
assert [item["target"]["sku"] for item in result["results"]] == ["bulk-selected"]
|
assert [item["target"]["sku"] for item in result["results"]] == ["bulk-selected"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_all_listings_from_non_whitelisted_statuses_respects_allowed_statuses(monkeypatch):
|
||||||
|
driver = InventoryManage({})
|
||||||
|
state = {"selected": [], "deleted": 0}
|
||||||
|
|
||||||
|
def fake_read_listing_status_rows_for_delete(shop_name, country, **kwargs):
|
||||||
|
return [
|
||||||
|
{"status": "商品信息草稿", "quantity": "2", "deleteQuantity": "", "processStatus": ""},
|
||||||
|
{"status": "缺少的信息", "quantity": "3", "deleteQuantity": "", "processStatus": ""},
|
||||||
|
{"status": "全部", "quantity": "9", "deleteQuantity": "", "processStatus": ""},
|
||||||
|
]
|
||||||
|
|
||||||
|
def fake_select_listing_status(status, **kwargs):
|
||||||
|
state["selected"].append(status)
|
||||||
|
|
||||||
|
def fake_delete_selected_filtered_inventory_rows(status, **kwargs):
|
||||||
|
state["deleted"] += 1
|
||||||
|
return {
|
||||||
|
"deletedCount": 1,
|
||||||
|
"target": {"sku": status, "rawText": status},
|
||||||
|
"successMessage": f"{status} 删除成功",
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(driver, "_read_listing_status_rows_for_delete", fake_read_listing_status_rows_for_delete)
|
||||||
|
monkeypatch.setattr(driver, "select_listing_status", fake_select_listing_status)
|
||||||
|
monkeypatch.setattr(driver, "_delete_selected_filtered_inventory_rows", fake_delete_selected_filtered_inventory_rows)
|
||||||
|
monkeypatch.setattr(driver, "_wait_inventory_loader", lambda timeout=5: None)
|
||||||
|
|
||||||
|
result = driver.delete_all_listings_from_non_whitelisted_statuses(
|
||||||
|
"郭亚芳",
|
||||||
|
"德国",
|
||||||
|
allowed_statuses=["缺少的信息"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert state["selected"] == ["缺少的信息"]
|
||||||
|
assert state["deleted"] == 3
|
||||||
|
assert result["deleteCounts"] == {"缺少的信息": 3}
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_all_listings_from_non_whitelisted_statuses_empty_allowed_statuses_deletes_nothing(monkeypatch):
|
||||||
|
driver = InventoryManage({})
|
||||||
|
state = {"selected": []}
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
driver,
|
||||||
|
"_read_listing_status_rows_for_delete",
|
||||||
|
lambda shop_name, country, **kwargs: [{"status": "商品信息草稿", "quantity": "2"}],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(driver, "select_listing_status", lambda status, **kwargs: state["selected"].append(status))
|
||||||
|
|
||||||
|
result = driver.delete_all_listings_from_non_whitelisted_statuses(
|
||||||
|
"郭亚芳",
|
||||||
|
"德国",
|
||||||
|
allowed_statuses=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert state["selected"] == []
|
||||||
|
assert result["deleteCandidates"] == []
|
||||||
|
assert result["totalDeleted"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_select_listing_status_uses_dropdown_option_not_visible_status(monkeypatch):
|
def test_select_listing_status_uses_dropdown_option_not_visible_status(monkeypatch):
|
||||||
driver = InventoryManage({})
|
driver = InventoryManage({})
|
||||||
calls = []
|
calls = []
|
||||||
@@ -606,7 +778,7 @@ def test_build_deletable_status_rows_keeps_non_whitelisted_statuses():
|
|||||||
{"status": "全部", "quantity": "9"},
|
{"status": "全部", "quantity": "9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
assert InventoryManage._build_deletable_status_rows(rows) == [
|
assert InventoryManage._build_deletable_status_rows(rows, ["库存状态删除"]) == [
|
||||||
{"status": "商品信息草稿", "canonicalStatus": "商品信息草稿", "quantity": 2},
|
{"status": "商品信息草稿", "canonicalStatus": "商品信息草稿", "quantity": 2},
|
||||||
{"status": "自定义异常状态", "canonicalStatus": "自定义异常状态", "quantity": 5},
|
{"status": "自定义异常状态", "canonicalStatus": "自定义异常状态", "quantity": 5},
|
||||||
]
|
]
|
||||||
@@ -621,7 +793,7 @@ def test_build_deletable_status_rows_skips_group_labels_without_quantity():
|
|||||||
{"status": "全部", "quantity": ""},
|
{"status": "全部", "quantity": ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
assert InventoryManage._build_deletable_status_rows(rows) == [
|
assert InventoryManage._build_deletable_status_rows(rows, ["库存状态删除"]) == [
|
||||||
{"status": "缺少的信息", "canonicalStatus": "缺少的信息", "quantity": 1},
|
{"status": "缺少的信息", "canonicalStatus": "缺少的信息", "quantity": 1},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -644,74 +816,65 @@ def test_build_deletable_status_rows_keeps_legacy_non_whitelist_statuses_deletab
|
|||||||
{"status": "订单页面已删除", "quantity": "3"},
|
{"status": "订单页面已删除", "quantity": "3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
assert InventoryManage._build_deletable_status_rows(rows) == [
|
assert InventoryManage._build_deletable_status_rows(rows, ["库存状态删除"]) == [
|
||||||
{"status": "详情页面已删除", "canonicalStatus": "详情页面已删除", "quantity": 1},
|
{"status": "详情页面已删除", "canonicalStatus": "详情页面已删除", "quantity": 1},
|
||||||
{"status": "缺少的信息", "canonicalStatus": "缺少的信息", "quantity": 2},
|
{"status": "缺少的信息", "canonicalStatus": "缺少的信息", "quantity": 2},
|
||||||
{"status": "订单页面已删除", "canonicalStatus": "详情页面已删除", "quantity": 3},
|
{"status": "订单页面已删除", "canonicalStatus": "详情页面已删除", "quantity": 3},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_deletable_status_rows_filters_by_delete_conditions():
|
||||||
|
rows = [
|
||||||
|
{"status": "商品信息草稿", "quantity": "2"},
|
||||||
|
{"status": "缺少的信息", "quantity": "5"},
|
||||||
|
{"status": "全部", "quantity": "9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert InventoryManage._build_deletable_status_rows(rows, ["商品信息草稿"]) == [
|
||||||
|
{"status": "商品信息草稿", "canonicalStatus": "商品信息草稿", "quantity": 2},
|
||||||
|
]
|
||||||
|
assert InventoryManage._build_deletable_status_rows(rows, []) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_deletable_complete_draft_tag_rows_respects_complete_draft_condition():
|
||||||
|
rows = [
|
||||||
|
{"tag": "未提交的草稿", "quantity": 1},
|
||||||
|
{"tag": "已提交:提供缺少的信息", "quantity": 2},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert InventoryManage._build_deletable_complete_draft_tag_rows(rows, ["已提交:提供缺少的信息"]) == [
|
||||||
|
{"tag": "已提交:提供缺少的信息", "canonicalTag": "已提交:提供缺少的信息", "quantity": 2},
|
||||||
|
]
|
||||||
|
assert InventoryManage._build_deletable_complete_draft_tag_rows(rows, ["补全草稿"]) == [
|
||||||
|
{"tag": "未提交的草稿", "canonicalTag": "未提交的草稿", "quantity": 1},
|
||||||
|
{"tag": "已提交:提供缺少的信息", "canonicalTag": "已提交:提供缺少的信息", "quantity": 2},
|
||||||
|
]
|
||||||
|
assert InventoryManage._build_deletable_complete_draft_tag_rows(rows, []) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_delete_condition_texts_accepts_frontend_payload_shapes():
|
||||||
|
assert PatrolDeleteTask._parse_delete_condition_texts(
|
||||||
|
[
|
||||||
|
{"id": 1, "conditionText": "商品信息草稿、缺少的信息"},
|
||||||
|
{"id": 2, "condition_text": "补全草稿"},
|
||||||
|
"详情页面已删除",
|
||||||
|
]
|
||||||
|
) == ["商品信息草稿", "缺少的信息", "补全草稿", "详情页面已删除"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_delete_condition_texts_defaults_to_empty_list_when_missing():
|
||||||
|
assert PatrolDeleteTask._resolve_delete_condition_texts({}) == []
|
||||||
|
|
||||||
|
|
||||||
def test_is_delete_success_message_accepts_both_copy_variants():
|
def test_is_delete_success_message_accepts_both_copy_variants():
|
||||||
assert InventoryManage._is_delete_success_message("1 个商品已经删除。所作更改需要15分钟才会显示在商品详情页面上")
|
assert InventoryManage._is_delete_success_message("1 个商品已经删除。所作更改需要15分钟才会显示在商品详情页面上")
|
||||||
assert InventoryManage._is_delete_success_message("所做更改需要 15 分钟才会显示在商品详情页面上。 1 个商品已删除。")
|
assert InventoryManage._is_delete_success_message("所做更改需要 15 分钟才会显示在商品详情页面上。 1 个商品已删除。")
|
||||||
|
|
||||||
|
|
||||||
def test_is_delete_success_message_accepts_deleted_count_without_delay_notice():
|
|
||||||
assert InventoryManage._is_delete_success_message("1 个商品已删除。")
|
|
||||||
assert InventoryManage._is_delete_success_message("已删除 2 个商品。")
|
|
||||||
assert not InventoryManage._is_delete_success_message("0 个商品已删除。")
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_delete_success_message_accepts_deleted_without_exact_count():
|
|
||||||
assert InventoryManage._is_delete_success_message("一个或多个商品已删除。")
|
|
||||||
assert InventoryManage._is_delete_success_message("一个或多个商品已经删除")
|
|
||||||
assert InventoryManage._extract_delete_success_count("一个或多个商品已删除。") == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_delete_success_count_accepts_success_message():
|
def test_extract_delete_success_count_accepts_success_message():
|
||||||
assert InventoryManage._extract_delete_success_count("所做更改需要 15 分钟才会显示在商品详情页面上。 3 个商品已删除。") == 3
|
assert InventoryManage._extract_delete_success_count("所做更改需要 15 分钟才会显示在商品详情页面上。 3 个商品已删除。") == 3
|
||||||
|
|
||||||
|
|
||||||
def test_short_error_simplifies_delete_success_message_timeout():
|
|
||||||
message = (
|
|
||||||
"状态[详情页面已删除]删除后未读取到成功提示: "
|
|
||||||
"['所做更改需要 15 分钟才会显示在商品详情页面上。 一个或多个商品已删除', "
|
|
||||||
"'莆田市城厢区从奇偶贸易...']"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert PatrolDeleteTask._short_error(message) == "删除后未读取到成功提示"
|
|
||||||
|
|
||||||
|
|
||||||
def test_short_error_simplifies_common_patrol_delete_failures():
|
|
||||||
examples = {
|
|
||||||
"检测到任务 12 的暂停请求,停止处理": "任务已中断",
|
|
||||||
"用户取消任务": "任务已中断",
|
|
||||||
"与页面的连接已断开: target frame detached": "浏览器连接中断",
|
|
||||||
"切换国家 德国 失败: element not found": "切换国家失败",
|
|
||||||
"未找到可用商品状态下拉框: {'ok': False, 'text': '...'}": "未找到商品状态下拉框",
|
|
||||||
"商品状态下拉框已打开但未读取到选项: {'ok': True}": "未读取到商品状态下拉选项",
|
|
||||||
"未找到商品状态选项[详情页面已删除]: {'rows': ['...']}": "未找到商品状态选项",
|
|
||||||
"状态[详情页面已删除]全选商品失败: {'ok': False, 'reason': '...'}": "全选商品失败",
|
|
||||||
"状态[详情页面已删除]点击批量删除失败: {'menuOpened': False, 'html': '...'}": "点击批量删除失败",
|
|
||||||
"状态[详情页面已删除]未出现删除确认弹窗": "未出现删除确认弹窗",
|
|
||||||
"当前页面对象不支持执行 JavaScript,无法探测 Shadow DOM": "当前页面无法执行 JavaScript",
|
|
||||||
}
|
|
||||||
|
|
||||||
for message, expected in examples.items():
|
|
||||||
assert PatrolDeleteTask._short_error(message) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_country_section_result_uses_simplified_delete_timeout_failure_reason():
|
|
||||||
section = PatrolDeleteTask._build_country_section_result(
|
|
||||||
country_name="英国",
|
|
||||||
status_rows=[{"status": "详情页面已删除", "quantity": "1"}],
|
|
||||||
delete_counts={},
|
|
||||||
default_process_status=f"处理失败: {PatrolDeleteTask._short_error('状态[详情页面已删除]删除后未读取到成功提示: []')}",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert section["rows"][0]["processStatus"] == "处理失败: 删除后未读取到成功提示"
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_country_section_result_sets_zero_delete_quantity_when_no_deletable_products():
|
def test_build_country_section_result_sets_zero_delete_quantity_when_no_deletable_products():
|
||||||
section = PatrolDeleteTask._build_country_section_result(
|
section = PatrolDeleteTask._build_country_section_result(
|
||||||
country_name="德国",
|
country_name="德国",
|
||||||
@@ -740,32 +903,6 @@ def test_build_country_section_result_sets_zero_delete_quantity_for_empty_no_del
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_build_country_section_result_marks_all_whitelisted_statuses_as_no_delete_needed():
|
|
||||||
whitelist_statuses = [
|
|
||||||
"全部",
|
|
||||||
"定价问题",
|
|
||||||
"需要批准",
|
|
||||||
"在搜索结果中禁止显示",
|
|
||||||
"配送问题",
|
|
||||||
"缺少报价",
|
|
||||||
"已停售",
|
|
||||||
"不可售",
|
|
||||||
"在售",
|
|
||||||
]
|
|
||||||
|
|
||||||
section = PatrolDeleteTask._build_country_section_result(
|
|
||||||
country_name="爱尔兰",
|
|
||||||
status_rows=[{"status": status, "quantity": "1"} for status in whitelist_statuses]
|
|
||||||
+ [{"status": "缺少的信息", "quantity": "2"}],
|
|
||||||
delete_counts={"缺少的信息": 2},
|
|
||||||
)
|
|
||||||
|
|
||||||
rows_by_status = {row["status"]: row for row in section["rows"]}
|
|
||||||
for status in whitelist_statuses:
|
|
||||||
assert rows_by_status[status]["deleteQuantity"] == "无需删除"
|
|
||||||
assert rows_by_status["缺少的信息"]["deleteQuantity"] == "2"
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_label_quantity_text_accepts_complete_draft_sample():
|
def test_parse_label_quantity_text_accepts_complete_draft_sample():
|
||||||
assert InventoryManage._parse_label_quantity_text("未提交的草稿 (0)") == ("未提交的草稿", 0)
|
assert InventoryManage._parse_label_quantity_text("未提交的草稿 (0)") == ("未提交的草稿", 0)
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,12 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-title">删除条件</div>
|
<div class="section-title condition-title">
|
||||||
|
<span>删除条件</span>
|
||||||
|
<span class="condition-title-hint">
|
||||||
|
注意删除条件的描述需要和页面描述一致
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="condition-panel">
|
<div class="condition-panel">
|
||||||
<div class="condition-input-row">
|
<div class="condition-input-row">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -823,13 +828,14 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!ids.length) {
|
if (!ids.length) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
const batch = await getPatrolDeleteTaskProgressBatch(ids);
|
const batch = await getPatrolDeleteTaskProgressBatch(ids);
|
||||||
mergeHistoryProgressItems(batch.items || []);
|
mergeHistoryProgressItems(batch.items || []);
|
||||||
if ((batch.missingTaskIds || []).length) {
|
if ((batch.missingTaskIds || []).length) {
|
||||||
await loadHistory();
|
await loadHistory();
|
||||||
}
|
}
|
||||||
|
return batch.missingTaskIds || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function startHistoryPolling() {
|
function startHistoryPolling() {
|
||||||
@@ -1029,7 +1035,11 @@ async function waitForTaskTerminal(taskId: number) {
|
|||||||
if (disposed) return "STOPPED";
|
if (disposed) return "STOPPED";
|
||||||
if (activeTaskId.value !== taskId) return "DELETED";
|
if (activeTaskId.value !== taskId) return "DELETED";
|
||||||
try {
|
try {
|
||||||
await refreshActiveTaskProgress([taskId]);
|
const missingTaskIds = await refreshActiveTaskProgress([taskId]);
|
||||||
|
if (missingTaskIds.includes(taskId)) {
|
||||||
|
clearActiveQueueTask();
|
||||||
|
return "DELETED";
|
||||||
|
}
|
||||||
if (activeTaskId.value !== taskId) return "DELETED";
|
if (activeTaskId.value !== taskId) return "DELETED";
|
||||||
if (transientErrorCount > 0) {
|
if (transientErrorCount > 0) {
|
||||||
queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待执行结果...`;
|
queuePushResult.value = `任务 ${taskId} 后端已恢复,继续等待执行结果...`;
|
||||||
@@ -1236,6 +1246,8 @@ onUnmounted(() => {
|
|||||||
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; }
|
.left-panel { width: 400px; background: #1e1e1e; padding: 20px; overflow-y: auto; border-right: 1px solid #2a2a2a; }
|
||||||
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
|
.right-panel { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #1a1a1a; }
|
||||||
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
.section-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
||||||
|
.condition-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||||
|
.condition-title-hint { flex: 1; min-width: 0; color: #d6a95a; font-size: 12px; text-align: right; line-height: 1.35; }
|
||||||
.input-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 16px; background: #252525; margin-bottom: 20px; }
|
.input-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 16px; background: #252525; margin-bottom: 20px; }
|
||||||
.hint { color: #888; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; }
|
.hint { color: #888; font-size: 12px; margin-bottom: 12px; line-height: 1.5; text-align: left; }
|
||||||
.input-row { display: flex; gap: 10px; align-items: center; }
|
.input-row { display: flex; gap: 10px; align-items: center; }
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>外观专利检测</title>
|
<title>外观专利检测</title>
|
||||||
<script type="module" crossorigin src="/assets/appearance-patent.js"></script>
|
<script type="module" crossorigin src="/assets/appearance-patent.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/appearance-patent-BPCrG75P.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/appearance-patent-TI-osB39.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -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
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
@@ -1 +0,0 @@
|
|||||||
const e=[{value:"SearchSuppressed",label:"在搜索结果中禁止显示"},{value:"ApprovalRequired",label:"需要批准"},{value:"Active",label:"在售"},{value:"DetailPageRemoved",label:"详情页面已删除"}];export{e as L};
|
|
||||||
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
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>格式转换 - 数富AI</title>
|
<title>格式转换 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
<link rel="stylesheet" crossorigin href="/assets/convert-r7MSXq7I.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据去重 - 数富AI</title>
|
<title>数据去重 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
<link rel="stylesheet" crossorigin href="/assets/dedupe-DMeaPUMZ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>删除品牌 - 数富AI</title>
|
<title>删除品牌 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BP3XWKAC.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
</head>
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-B5ChOXZY.css">
|
||||||
<body>
|
</head>
|
||||||
<div id="app"></div>
|
<body>
|
||||||
</body>
|
<div id="app"></div>
|
||||||
</html>
|
|
||||||
|
</body>
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>巡店删除 - 数富AI</title>
|
<title>巡店删除 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/patrol-delete.js"></script>
|
<script type="module" crossorigin src="/assets/patrol-delete.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-CuE9Zzgz.js">
|
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BbuLS91u.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/patrol-delete-CSekhzSS.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/patrol-delete-DyumfGaZ.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>跟价 - 数富AI</title>
|
<title>跟价 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/price-track.js"></script>
|
<script type="module" crossorigin src="/assets/price-track.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/price-track-z8RU_FlW.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/price-track-DMhhVz4u.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>商品风险解决 - 数富AI</title>
|
<title>商品风险解决 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/product-risk.js"></script>
|
<script type="module" crossorigin src="/assets/product-risk.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/product-risk-eXMv239F.css">
|
<link rel="stylesheet" crossorigin href="/assets/product-risk-CI6qY5U2.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>巡店删除 - 数富AI</title>
|
<title>巡店删除 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/query-asin.js"></script>
|
<script type="module" crossorigin src="/assets/query-asin.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-CuE9Zzgz.js">
|
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BbuLS91u.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/query-asin-Bv7Jtgqa.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/query-asin-Bw6BQsRN.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>匹配店铺 - 数富AI</title>
|
<title>匹配店铺 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/shop-match.js"></script>
|
<script type="module" crossorigin src="/assets/shop-match.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-CuE9Zzgz.js">
|
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BbuLS91u.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/shop-match-BiZP8IGM.css">
|
<link rel="stylesheet" crossorigin href="/assets/shop-match-CxCKUsqS.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据拆分 - 数富AI</title>
|
<title>数据拆分 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
<link rel="stylesheet" crossorigin href="/assets/split-Cw6sXhAg.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
|
||||||
</html>
|
</body>
|
||||||
|
|||||||
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version": "1.0.56"}
|
||||||
Reference in New Issue
Block a user