Files
atomk-hermes-skills/skills/archived/tongtool-y2-lowest-shipping/SKILL.md
T

17 KiB

name, description
name description
tongtool-y2-lowest-shipping Batch process TEMU semi-managed orders in Tongtool ERP to calculate the lowest shipping price, skipping printed/shipped orders, and saving remarks using Web UI + API fallback.

Tongtool TEMU Lowest Shipping Calculator & Remark

This skill automates the process of finding the lowest shipping method for TEMU semi-managed orders in Tongtool, skipping orders that are already processed, utilizing the TEMU platform API for calculation, and saving the result robustly using a Web UI + API Fallback mechanism.

Prerequisites

  • Python 3
  • DrissionPage
  • ddddocr
  • requests

Crucial Platform Integration Discoveries & Pitfalls

1. Tongtool Open API V3 ordersQuery Signature Bug

When querying /openapi/tongtool/ordersQuery, passing date filters (saleDate, payDate, updatedDate, etc.) frequently results in a 519 sign fail or 525 cannot be all empty error. The official V3 signature logic is broken for this specific endpoint when url-encoding date parameters. Workaround: Fetch orders from pre-existing local caches (e.g., scraping results), or query by specific orderId, or query strictly by orderStatus: "1" (Processing) without any date parameters.

2. Business Logic for Skipping Orders (Order Status Rules)

Do not perform shipping calculations for orders if they meet any of these criteria based on the API ordersQuery details response:

  • orderStatus is shipped (物流商发货), completed (签收), or cancelled.
  • printCompleteTime has a value (通途已打印).
  • despatchCompleteTime has a value (通途已发货).
  • The order already has a remark containing [H201试算] (assuming it isn't an error marker).

3. Robust Remark Writing (Web UI + API Fallback)

Because Tongtool's Web UI is highly asynchronous, textarea injection and Javascript .click() on the save button occasionally fails silently. The robust pipeline must:

  1. Write the remark via Web UI layout injection.
  2. Wait a few seconds array.
  3. Verify via API (/openapi/tongtool/ordersRemarkQuery) using orderIdKey.
  4. If missing, Fallback to API write (/openapi/tongtool/addOrderRemark).

The Pipeline Script

This script logs in, filters and sorts target orders (ascending by the last 4 digits of the order ID), extracts the cheapest price via the shipping analysis popup, and securely writes it back.

import time, json, re, hashlib, requests, os
from DrissionPage import ChromiumPage, ChromiumOptions
import ddddocr

ACCESS_KEY = "b4163dfec4654fd480c1cadeafea8ae7"
SECRET_KEY = "0fe4f51c1b8840e6bd70394ce6ec74f638a25524decb45b2adbb7ae5d56bfcdf"
PARTNER_OPEN_ID = "ec1d0a0ac9bceca98ed62e1504351371"
BASE_URL = "https://open.tongtool.com/apiv3-service"

SUCCESS_PREFIX = '[H201试算]'
ERROR_REMARK = '[H201试算]出错没找到最低价'

def get_token_and_sign():
    r1 = requests.get(
        f"https://open.tongtool.com/open-platform-service/devApp/appToken?accessKey={ACCESS_KEY}&secretAccessKey={SECRET_KEY}",
        headers={"Content-Type": "application/json"}
    )
    app_token = r1.json()["datas"]
    timestamp = str(int(time.time() * 1000))
    raw_sign = f"app_token{app_token}timestamp{timestamp}{SECRET_KEY}"
    sign = hashlib.md5(raw_sign.encode()).hexdigest()
    return {"app_token": app_token, "timestamp": timestamp, "sign": sign}

def api_call(path, body):
    _params = get_token_and_sign()
    headers = {"Content-Type": "application/json", "api_version": "3.0"}
    for _ in range(3):
        try:
            resp = requests.post(f"{BASE_URL}{path}", params=_params, headers=headers, json=body, timeout=20)
            return resp.json()
        except:
            time.sleep(2)
    return {}

def filter_and_sort_orders(raw_orders_list, limit=5):
    """Filter out completed/shipped/printed orders, and sort by ID last 4 digits"""
    unique_orders = list(set(raw_orders_list))
    valid_pool = []
    
    for oid in unique_orders:
        res = api_call("/openapi/tongtool/ordersQuery", {
            "merchantId": PARTNER_OPEN_ID, "pageNo": "1", "pageSize": "100", "orderId": oid
        })
        if res.get("code") != 200 or not res.get("datas", {}).get("array"):
            continue
            
        info = res["datas"]["array"][0]
        
        # 1. Skip based on status and timestamps
        if info.get("orderStatus") in ["shipped", "completed", "cancelled"]:
            continue
        if info.get("printCompleteTime"):
            continue
        if info.get("despatchCompleteTime"):
            continue
            
        # 2. Skip already processed ones
        marks_res = api_call("/openapi/tongtool/ordersRemarkQuery", {
            "merchantId": PARTNER_OPEN_ID, "orderIdKey": info.get("orderIdKey"), 
            "pageNo": "1", "pageSize": "10"
        })
        
        already_success = False
        if marks_res.get("code") == 200 and marks_res.get("datas"):
            for m in marks_res["datas"].get("array", []):
                text = m.get("orderRemark", "")
                if SUCCESS_PREFIX in text and '出错' not in text:
                    already_success = True
                    break
        
        if already_success: continue
        valid_pool.append(oid)
    
    # Sort ascending by the last 4 digits
    def last_4(o):
        m = re.search(r'(\\d{4})$', o)
        return int(m.group(1)) if m else 9999
        
    valid_pool.sort(key=last_4)
    return valid_pool[:limit]

def calculate_temu_lowest_shipping_and_remark(order_id: str, driver):
    def nuke_popups(page):
        try:
            page.run_js("""
                document.querySelectorAll('.layui-layer-shade, .layui-layer, .modal, .overlay, .jclose-blue, .close, [title="关闭"], .layui-layer-close').forEach(el => { try { el.remove(); } catch(e){} });
            """)
        except: pass

    driver.get("https://yijiety.tongtool.com/dashboard/homepage/index.htm")
    time.sleep(4)
    nuke_popups(driver)

    search_url = (
        f"https://yijiety.tongtool.com/search/order.htm"
        f"?search_text_value={order_id}&search_mark=salesRecordNumber&source=global"
    )
    driver.get(search_url)
    time.sleep(8)
    nuke_popups(driver)

    def open_shipping_popup():
        driver.run_js("""
            for (const el of document.querySelectorAll('*')) {
                if (el.textContent.trim() === '仓储物流' && el.offsetHeight > 0) { el.click(); return true; }
            }
            return false;
        """)
        time.sleep(3)
        driver.run_js("""
            for (const el of document.querySelectorAll('a, button, span')) {
                if (el.textContent.trim() === '编辑') {
                    const r = el.getBoundingClientRect();
                    if (r.width > 0 && r.height > 0 && r.height < 80) { el.click(); return true; }
                }
            }
            return false;
        """)
        time.sleep(4)
        clicked_analysis = driver.run_js("""
            for (const a of document.querySelectorAll('a')) {
                if (a.textContent.trim() === '邮寄方式解析') { a.click(); return true; }
            }
            return false;
        """)
        if not clicked_analysis: return False
        time.sleep(6)
        return driver.run_js("return !!document.querySelector('div[windowid] #tabpanelDiv');")

    if not open_shipping_popup(): return None

    def extract_temu_cheapest():
        try:
            driver.run_js("""
                const popups = document.querySelectorAll('div[windowid]');
                let popup = null;
                for (const p of popups) { if (p.querySelector('#tabpanelDiv')) { popup = p; break; } }
                if (!popup) return;
                const tab = popup.querySelector('li[tabtitle="TEMU平台接口试算"]');
                if (tab) tab.click();
            """)
            time.sleep(4)
            driver.run_js("""
                const popups = document.querySelectorAll('div[windowid]');
                let popup = null;
                for (const p of popups) { if (p.querySelector('#tabpanelDiv')) { popup = p; break; } }
                if (!popup) return;
                const temuDiv = popup.querySelector('#temuShippingMethod');
                if (!temuDiv || temuDiv.classList.contains('hide')) return;
                for (const b of temuDiv.querySelectorAll('a, button')) {
                    if (b.textContent.includes('查询') || b.textContent.includes('试算')) { b.click(); return; }
                }
            """)
            time.sleep(8)
            prices = driver.run_js("""
                const popup = Array.from(document.querySelectorAll('div[windowid]')).find(p => p.querySelector('#tabpanelDiv'));
                if (!popup) return [];
                const temuDiv = popup.querySelector('#temuShippingMethod') || popup;
                const out = [];
                for (const tr of temuDiv.querySelectorAll('tr')) {
                    const tds = tr.querySelectorAll('td');
                    if (tds.length < 3) continue;
                    const rowText = tr.textContent || '';
                    
                    let priceMatch = rowText.match(/\\$\\s*(\\d+\\.\\d{2})/);
                    let currency = '$';
                    if (!priceMatch) {
                        priceMatch = rowText.match(/[¥¥]\\s*(\\d+\\.\\d{2})/);
                        currency = '¥';
                    }
                if (!priceMatch) continue;
                
                // Skip disabled options ("未启用") entirely
                if (rowText.includes('未启用')) continue;
                
                const price = parseFloat(priceMatch[1]);
                    if (isNaN(price) || price <= 0) continue;

                    let carrier = '', method = '';
                    for (const td of tds) {
                        const t = td.textContent.trim();
                        if (!t) continue;
                        if (t.includes(priceMatch[0])) continue;
                        if (!carrier) { carrier = t; continue; }
                        if (!method)  { method  = t; break; }
                    }
                    if (!carrier && !method) continue;
                    out.push({carrier, method, price, raw: priceMatch[0], currency});
                }
                return out;
            """)
            if not prices: return None
            return min(prices, key=lambda x: x['price'])
        except Exception as e: return None

    cheapest = extract_temu_cheapest()
    
    driver.run_js("""
        document.querySelectorAll('.jclose-blue, .close, [title="关闭"], div[windowid] .layui-layer-close').forEach(b => { try { b.click(); } catch(e){} });
        document.querySelectorAll('div[windowid], .layui-layer, .layui-layer-shade').forEach(e => e.remove());
    """)
        time.sleep(2)
        
    if cheapest:
        c = (cheapest.get('carrier') or '').strip()
        m = (cheapest.get('method') or '').strip()
        label = f"{c}>>{m}" if c and m else (c or m)
        remark = f"{SUCCESS_PREFIX}{label} {cheapest['currency']}{cheapest['price']:.2f}"
        
        # --- NEW LOGIC: Update order's actual shipping method in Tongtool based on lowest price ---
        # NOTE: Tongtool automatically handles binding the top-level logistics provider (e.g. 湖北众信 for US East, 易世通达 for US West).
        # We MUST NOT change the top-level provider. We only select the correct last-mile option (USPS, GOFO, Swift, etc.) associated with that existing provider.
        def get_best_method_code(method_str):
            # Method mapping: match common last-mile carrier names
            method_upper = method_str.upper()
            sub_method = None
            if "USPS" in method_upper:
                sub_method = "USPS"
            elif "GOFO" in method_upper:
                sub_method = "GOFO"
            elif "SWIFT" in method_upper:
                sub_method = "SwiftX" if "SWIFTX" in method_upper else "Swift"
            elif "SPEED" in method_upper:
                sub_method = "SpeedX" if "SPEEDX" in method_upper else "Speed"
            else:
                return method_str
            
            return sub_method
            
        def update_shipping_method(driver, best_sub_method):
            if not best_sub_method:
                return False
                
            try:
                # Check current selected method first
                current_method_full = driver.run_js("""
                    // The shipping method text input
                    const inputs = document.querySelectorAll('input.layui-input');
                    for (const input of inputs) {
                        if (input.value.includes('>>')) {
                            return input.value;
                        }
                    }
                    return "";
                """)
                
                if current_method_full and best_sub_method.upper() in current_method_full.upper():
                     return True # Already correct
                     
                # Parse current provider to stay within it (e.g. "湖北众信" or "易世通达")
                current_provider = None
                if current_method_full:
                    if "湖北众信" in current_method_full:
                        current_provider = "湖北众信"
                    elif "易世通达" in current_method_full:
                        current_provider = "易世通达"
                        
                if not current_provider:
                     # If we can't extract the current provider, we can't safely ensure we stay within it.
                     return False
                     
                # Find dropdown trigger and click it
                opened = driver.run_js("""
                   const inputs = document.querySelectorAll('input.layui-input');
                   for(const input of inputs) {
                       if(input.value.includes('>>') || input.placeholder.includes('方式')) {
                           input.click();
                           return true;
                       }
                   }
                   return false;
                """)
                if not opened: return False
                time.sleep(1)
                
                # Click the option (MUST contain both the current provider and the new best sub_method)
                clicked = driver.run_js(f"""
                    const items = document.querySelectorAll('dd');
                    for (const item of items) {{
                        const text = item.textContent.toUpperCase();
                        if (text.includes('{current_provider}') && text.includes('{best_sub_method.upper()}')) {{
                            item.click();
                            return true;
                        }}
                    }}
                    return false;
                """)
                
                if clicked:
                    # Save changes for the entire order
                    driver.run_js("""
                        for (const b of document.querySelectorAll('button')) {
                            if (b.textContent.trim() === '保存' && !b.closest('#orderRemarkForm')) {
                                b.click();
                                return true;
                            }
                        }
                        return false;
                    """)
                    time.sleep(2)
                    return True
                return False
            except Exception as e:
                return False
        
        target_sub_method = get_best_method_code(m)
        update_shipping_method(driver, target_sub_method)
        # -----------------------------------------------------------------------------------------
        
    else: remark = ERROR_REMARK
        
    remark_btn = driver.ele('css:#addOrderRemark')
    if remark_btn:
        a_tag = remark_btn.ele('tag:a')
        if a_tag:
            a_tag.click()
            time.sleep(2)
            textarea = driver.ele('tag:textarea')
            if textarea:
                js = f'''
                    var ta = document.querySelector('textarea');
                    if(ta) {{ ta.value = '{remark}'; ta.dispatchEvent(new Event('input', {{bubbles: true}})); ta.dispatchEvent(new Event('change', {{bubbles: true}})); }}
                '''
                driver.run_js(js)
                time.sleep(1)
                for a in driver.eles('tag:a'):
                    if a.text.strip() == '保存':
                        a.click(by_js=True)
                        time.sleep(3)
                        break
    return remark

# Example usage integrating verify & fallback...
# order_id_key can be fetched via generic ordersQuery beforehand.
# remark = calculate_temu_lowest_shipping_and_remark(order_id, driver)
# -> Verify -> api_call("/openapi/tongtool/ordersRemarkQuery", ...)
# -> Fallback -> api_call("/openapi/tongtool/addOrderRemark", ...)