import json import os import sys import threading import time from pathlib import Path from types import SimpleNamespace import requests ROOT_DIR = Path(__file__).resolve().parent APP_DIR = ROOT_DIR / "app" sys.path.insert(0, str(APP_DIR)) from amazon.main import TaskMonitor # noqa: E402 from amazon.patrol_delete import InventoryManage, PatrolDeleteTask # noqa: E402 from config import ( # noqa: E402 DELETE_BRAND_API_BASE, JAVA_API_BASE, JSON_TASK_QUEUE, ZN_COMPANY, ZN_PASSWORD, ZN_USERNAME, runing_task, ) from main import WindowAPI # noqa: E402 SHOP_NAME = "郭亚芳" USER_ID = 1 COUNTRIES = [item.strip() for item in os.environ.get("PATROL_COUNTRIES", "德国,西班牙,意大利").split(",") if item.strip()] TS_MODE = os.environ.get("TS_MODE", "full").strip().lower() class EventSlot: def __iadd__(self, handler): return self class DummyWindow: def __init__(self): self.events = SimpleNamespace(maximized=EventSlot(), restored=EventSlot()) def log(message, payload=None): print(f"[ts] {message}", flush=True) if payload is not None: print(json.dumps(payload, ensure_ascii=False, indent=2), flush=True) def unwrap(response): log(f"HTTP {response.request.method} {response.url} -> {response.status_code}") response.raise_for_status() data = response.json() if not data.get("success"): raise RuntimeError(data.get("message") or data.get("error") or response.text) return data.get("data") def get_match_item(): data = unwrap( requests.post( f"{JAVA_API_BASE}/api/patrol-delete/match-shops", json={"user_id": USER_ID, "shop_names": [SHOP_NAME]}, timeout=40, ) ) items = data.get("items") or [] if not items: raise RuntimeError("match-shops did not return any items") item = items[0] log("match result", item) if not item.get("matched"): raise RuntimeError(f"shop is not matched: {item}") return item def create_java_task(match_item): country_sections = [ { "country": country, "rows": [ { "status": "全部", "quantity": "", "deleteQuantity": "", "processStatus": "", } ], } for country in COUNTRIES ] cart_ratios = [{"country": country, "ratio": ""} for country in COUNTRIES] task_item = { "shopName": match_item.get("shopName") or SHOP_NAME, "matched": bool(match_item.get("matched")), "shopId": match_item.get("shopId"), "platform": match_item.get("platform"), "companyName": match_item.get("companyName"), "matchStatus": match_item.get("matchStatus"), "matchMessage": match_item.get("matchMessage"), "countrySections": country_sections, "cartRatios": cart_ratios, } data = unwrap( requests.post( f"{JAVA_API_BASE}/api/patrol-delete/tasks", json={"user_id": USER_ID, "items": [task_item]}, timeout=40, ) ) log("created task", data) return data def build_queue_payload(created_task): task_id = created_task["taskId"] created_item = (created_task.get("items") or [])[0] sections = created_item.get("countrySections") or [] ratios = created_item.get("cartRatios") or [] payload = { "type": "patrol-delete-run", "ts": int(time.time() * 1000), "data": { "taskId": task_id, "user_id": USER_ID, "source": "ts-real-patrol-delete", "items": [ { "shopName": created_item.get("shopName") or SHOP_NAME, "shopId": created_item.get("shopId"), "platform": created_item.get("platform"), "companyName": created_item.get("companyName"), "matched": created_item.get("matched"), "matchStatus": created_item.get("matchStatus"), "matchMessage": created_item.get("matchMessage"), } ], "template_rows": [ { "shopName": created_item.get("shopName") or SHOP_NAME, "countries": [ { "country": section.get("country"), "status": (section.get("rows") or [{}])[0].get("status", ""), "quantity": (section.get("rows") or [{}])[0].get("quantity", ""), "deleteQuantity": (section.get("rows") or [{}])[0].get("deleteQuantity", ""), "processStatus": (section.get("rows") or [{}])[0].get("processStatus", ""), } for section in sections ], "cartRatios": ratios, } ], "country_sections": sections, "cart_ratios": ratios, }, } log("queue payload", payload) return payload def make_monitor_without_startup_kill(): monitor = object.__new__(TaskMonitor) monitor.running = True monitor.user_info = { "company": ZN_COMPANY, "username": ZN_USERNAME, "password": ZN_PASSWORD, } monitor.chunk_index = 1 monitor.max_workers = 1 monitor.executor = None return monitor def poll_java_task(task_id): for _ in range(60): try: data = unwrap( requests.post( f"{JAVA_API_BASE}/api/patrol-delete/tasks/progress/batch", json={"taskIds": [task_id]}, timeout=40, ) ) log("java progress", data) items = data.get("items") or [] if items and items[0].get("taskStatus") in {"SUCCESS", "FAILED", "COMPLETED"}: return items[0] except Exception as exc: log(f"java progress poll failed: {exc}") time.sleep(5) return None def drain_queue(): while True: try: JSON_TASK_QUEUE.get_nowait() except Exception: return def make_user_info(company_name=None): return { "company": company_name or ZN_COMPANY, "username": ZN_USERNAME, "password": ZN_PASSWORD, } def complete_draft_payload_rows(tags): rows = [] for item in tags: rows.append( { "status": "补全草稿", "tag": item.get("tag", ""), "type": "completeDraft", "quantity": str(item.get("quantity") if item.get("quantity") is not None else ""), "deleteQuantity": str(item.get("quantity") if item.get("quantity") is not None else ""), "processStatus": "已完成", "raw_text": item.get("raw_text", ""), } ) return rows def run_complete_draft_real_browser(): log("starting real complete-draft read test") log("scope", {"shop": SHOP_NAME, "countries": COUNTRIES, "user_id": USER_ID}) match_item = get_match_item() driver = InventoryManage(make_user_info(match_item.get("companyName"))) try: browser = driver.open_shop(match_item.get("shopName") or SHOP_NAME) if not browser or browser == "店铺不存在": raise RuntimeError(f"open shop failed: {browser}") if driver.need_login(): raise RuntimeError("shop requires login; complete-draft read test cannot continue safely") normalized_sections = [] for country in COUNTRIES: log(f"switching country for complete-draft read: {country}") if not driver.switch_to_country(country): raise RuntimeError(f"switch country failed: {country}") tags = driver.get_complete_draft_quick_view_tags(match_item.get("shopName") or SHOP_NAME, country) payload_rows = complete_draft_payload_rows(tags) normalized = PatrolDeleteTask._normalize_country_sections_for_result( [{"country": country, "rows": payload_rows}] )[0] normalized_sections.append(normalized) log( "complete draft country result", { "country": country, "rawTags": tags, "payloadRowsBeforeNormalize": payload_rows, "normalizedForSubmit": normalized, }, ) log("complete draft normalized sections", normalized_sections) finally: try: driver.close_store() except Exception as exc: log(f"close store failed: {exc}") def main(): if TS_MODE == "complete-draft": run_complete_draft_real_browser() return log("starting real patrol-delete automation") log("scope", {"shop": SHOP_NAME, "countries": COUNTRIES, "user_id": USER_ID}) log("java api base", {"JAVA_API_BASE": JAVA_API_BASE, "DELETE_BRAND_API_BASE": DELETE_BRAND_API_BASE}) log("ziniao env", {"company": bool(ZN_COMPANY), "username": bool(ZN_USERNAME), "password": bool(ZN_PASSWORD)}) drain_queue() match_item = get_match_item() created_task = create_java_task(match_item) task_id = created_task["taskId"] payload = build_queue_payload(created_task) monitor = make_monitor_without_startup_kill() monitor_thread = threading.Thread(target=monitor.start, daemon=True) monitor_thread.start() time.sleep(0.5) enqueue_result = WindowAPI(DummyWindow()).enqueue_json(payload) log("enqueue result", enqueue_result) if not enqueue_result.get("success"): raise RuntimeError(f"enqueue failed: {enqueue_result}") deadline = time.time() + 60 * 45 last_status = None while time.time() < deadline: state = runing_task.get(task_id) if state and state != last_status: log("python task state", state) last_status = dict(state) if state and state.get("status") in {"completed", "failed", "stopped"}: break time.sleep(5) monitor.running = False JSON_TASK_QUEUE.put({"type": "__stop__"}) monitor_thread.join(timeout=10) log("python final state", runing_task.get(task_id)) final_java = poll_java_task(task_id) log("java final", final_java) log("finished", {"taskId": task_id}) if __name__ == "__main__": main()