Files

20 KiB
Raw Permalink Blame History

name, description
name description
tongtool-erp-automation Automate Tongtool (通途) ERP using DrissionPage — login, CAPTCHA solving, tab switching, iframe navigation, and popup handling.

Tongtool ERP Automation

Use this skill when the user asks to log in, navigate, or extract data from the Tongtool ERP system (tongtool.com).

Setup

Ensure DrissionPage and ddddocr are installed.

from DrissionPage import ChromiumPage, ChromiumOptions
import ddddocr, os, shutil, time

co = ChromiumOptions()
co.set_browser_path('/home/ubuntu/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome')
co.set_argument('--no-sandbox')
co.set_argument('--disable-dev-shm-usage')
co.headless()
co.auto_port()
driver = ChromiumPage(co)

1. Login — Two Methods

1a. Headless DrissionPage Login + CAPTCHA (Server-side)

Target: https://passport.tongtool.com/ Credentials are usually ozonezsjgw@163.com / 111111.

import re, requests

def correct_ocr(raw_code):
    """Fix common ddddocr misreads for Tongtool captcha."""
    return raw_code.translate(str.maketrans('oliOSsZz', '01105522'))

def login(driver, user, pwd, max_retry=5):
    for attempt in range(max_retry):
        driver.get('https://passport.tongtool.com/')
        time.sleep(3)
        driver.ele('css:input[name="username"]').input(user)
        driver.ele('css:input[name="password"]').input(pwd)
        
        # CAPTCHA: selector is img.pic-yzm
        img = driver.ele('css:img.pic-yzm')
        src = img.attr('src')
        cookies = {c['name']: c['value'] for c in driver.cookies()}
        resp = requests.get(src, cookies=cookies, timeout=10)
        raw = ddddocr.DdddOcr(show_ad=False).classification(resp.content)
        code = correct_ocr(raw)
        driver.ele('css:input[name="captcha"]').input(code)
        
        driver.ele('css:button.btn-primary').click()
        time.sleep(6)
        
        # Wait for redirect from /check to /member
        for _ in range(5):
            if 'member' in driver.url:
                return True
            time.sleep(2)
        
        # Check if still on login page
        if 'passport' in driver.url:
            body = driver.ele('tag:body').text
            if '验证码输入错误' in body:
                continue  # retry
            elif '密码' in body and '错误' in body:
                raise ValueError('Wrong password')
    return False

Key points:

  • Captcha image selector: img.pic-yzm (not @src^=/api/common/code).
  • Login button: button.btn-primary (not text:立即登录).
  • After submit, URL lands on /check then redirects to member.tongtool.com — wait for member in URL.
  • correct_ocr() fixes common ddddocr misreads (o→0, l→1, O→0, S→5, s→5, Z→2, z→2).

1b. CDP-Based Login via Bridge (Alternative — Desktop-side)

Use this when headless login is blocked by device verification or captcha. The Desktop Chrome is already a trusted device, so no captcha or device approval is needed.

Prerequisites: A Desktop slot connected to Bridge with CDP enabled. Trigger /cdp/snapshot first to establish CDP session.

Key CDP evaluate response structure: r['result']['result']['value'] — Bridge returns {ok: true, result: {result: {type: "...", value: ...}}}.

Flow:

  1. Navigate: POST /cdp/navigate -> https://passport.tongtool.com/
  2. Fill username via native setter:
    Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set.call(inp, 'ozonezsjgw@163.com')
  3. Fill password via native setter
  4. Click login button by text 立即登录
  5. Wait for redirect: member.tongtool.com
  6. Navigate to ERP: https://yijiety.tongtool.com/dashboard/homepage/index.htm

Full Python helper:

def evaluate(js, slot='desktop-mq27qgb0'):
    r = bridge_post('/cdp/evaluate', {'slot': slot, 'expression': js})
    if r.get('ok') and r.get('result') and r['result'].get('result'):
        return r['result']['result'].get('value')
    return r

2. Enter ERP

After successful login (member.tongtool.com), the simplest and most reliable way to enter the ERP is direct navigation — no tab switching needed.

# Method 1 (Recommended): Direct navigation
driver.get("https://yijiety.tongtool.com/dashboard/homepage/index.htm")
time.sleep(8)  # SPA takes time to bootstrap

# Verify by checking body text
body = driver.ele('tag:body').text
if '发货管理' in body:
    print("✅ ERP loaded")
else:
    print("❌ ERP not loaded")

Deprecated: The old "click 进入系统 then switch tab" approach is flaky in headless mode. Direct navigation is faster and more stable because session cookies are already set.

3. TEMU Semi-Managed Order — Bulk Shipping Info & Remark

Complete workflow: login → wait stock page → filter TEMU → read shipping → add remark

Typical scenario: The user wants to find the cheapest shipping method for TEMU semi-managed orders in "等待配货" status and record it in each order's remark.

Critical finding: For TEMU semi-managed orders in 等待配货 status, the system has already auto-calculated and assigned the cheapest shipping method (e.g., 湖北众信 EJET-SpeedX). There is NO need to trigger an explicit "运费试算" — the logistics provider, tracking method, and price are already visible in the order detail panel once you click the order row.

3.1 Navigate to Wait Stock page

driver.get("https://yijiety.tongtool.com/warehouse/assignStockSelfMaintenance/index.htm")
time.sleep(10)

3.2 Set warehouse via hidden input (custom t_combo_box)

The warehouse dropdown is a custom t_combo_box component that cannot be triggered by standard clicks. The reliable method is:

def set_warehouse(driver, warehouse_name):
    """Set warehouse by manipulating hidden input + clicking the TD cell."""
    # Option A: use the hidden input directly
    js = f"""
        var hidden = document.querySelector('input[name="warehouseSelect"]');
        if (hidden) {{
            hidden.value = '{warehouse_name}';
            var event = new Event('change', {{ bubbles: true }});
            hidden.dispatchEvent(event);
            return 'set via hidden input';
        }}
        return 'hidden input not found';
    """
    result = driver.run_js(js)
    time.sleep(2)

    # Option B: click the TD that contains the warehouse text
    js2 = f"""
        var tds = document.querySelectorAll('td');
        for (var i=0; i<tds.length; i++) {{
            if (tds[i].textContent.trim() === '{warehouse_name}') {{
                tds[i].click();
                return 'clicked TD: ' + tds[i].textContent;
            }}
        }}
        return 'TD not found';
    """
    result2 = driver.run_js(js2)
    time.sleep(2)
    return result, result2

set_warehouse(driver, "武汉仓")

Why this works: The component stores the selected value in a hidden input[name="warehouseSelect"]. Setting it + dispatching change updates the model. Clicking the corresponding <td> in the dropdown list confirms the selection in the UI. The allWarehouseInfo JavaScript object on the page contains warehouse metadata (IDs, names, etc.) if you need to map names to IDs.

3.3 Filter by channel and status

def click_by_text(driver, text, tag="*"):
    js = f"""
        var els = document.querySelectorAll('{tag}');
        for (var i=0; i<els.length; i++) {{
            if (els[i].textContent.trim() === '{text}') {{
                els[i].click();
                return 'clicked: ' + els[i].textContent;
            }}
        }}
        return 'not found: {text}';
    """
    return driver.run_js(js)

# Click TEMU channel tab
click_by_text(driver, "temu")
time.sleep(3)

# Click 新订单 status
click_by_text(driver, "新订单")
time.sleep(3)

# Click 查询 button
click_by_text(driver, "查询", "button")
time.sleep(5)

3.4 Extract order IDs from page source (NOT DOM)

The SPA renders order rows asynchronously. DOM-based extraction often fails because the grid is a custom component. Instead, extract from the raw page text or JavaScript variables embedded in the HTML:

body_text = driver.ele('tag:body').text
# Or use page source if body text doesn't contain orders
source = driver.html

# Orders appear as hyphenated IDs like: 88NeoHab-PO-211-16060374487670113
import re
orders = list(set(re.findall(r'[A-Za-z0-9]+-PO-\d+-\d+', body_text)))
print(f"Found {len(orders)} orders: {orders}")

Alternative: Search the raw HTML for salesRecordNumber or recordNumber fields if the regex doesn't match your order ID format.

3.5 For each order: open detail → read shipping → add remark

for order_id in orders:
    # Navigate to global search for this order
    search_url = (
        f"https://yijiety.tongtool.com/search/order.htm"
        f"?search_text_value={order_id}"
        f"&search_mark=salesRecordNumber"
        f"&source=global"
    )
    driver.get(search_url)
    time.sleep(10)

    # Open "邮寄方式分析" popup to get TEMU platform API price
    # IMPORTANT: The trigger button for this popup is NOT discoverable via DOM queries.
    # If the user provides the platform API price from a screenshot, use that directly
    # instead of extracting from the order detail page.
    #
    # Popup path (manual): Order detail → click "邮寄方式分析" → select "TEMU平台接口试算" tab
    # The table shows rows like:
    #   USPS,Ground Advantage | 预估$2.962-5个工作日送达 | TEMU半托管美东>>USPS,Ground Advantage...
    #
    # The price is already in USD (e.g., $2.96) — NO currency conversion needed.
    #
    # If you must fall back to the system logistics panel (less accurate):
    #   click_by_text(driver, order_id); time.sleep(5)
    #   Expand 仓储物流 section and parse CNY price, then divide by 6.8232 (not recommended).

    usd_price = 2.96  # Replace with actual popup value provided by user
    provider = "USPS Ground Advantage"
    weight = "0.12lb"  # From popup form field "包裹重量(lb"

    # Construct remark
    # Format: [运费试算]TEMU平台接口: {物流商} / USD {金额} / {重量}
    NOTE_TEXT = f"[运费试算]TEMU平台接口: {provider} / USD {usd_price} / {weight}"

    # Add remark
    remark_btn = driver.ele('css:#addOrderRemark')
    anchor = remark_btn.ele('tag:a')
    anchor.click()
    time.sleep(5)

    textarea = driver.ele('tag:textarea')
    textarea.input(NOTE_TEXT)
    time.sleep(1)

    for a in driver.eles('tag:a'):
        if a.text.strip() == '保存':
            a.click()
            break
    time.sleep(3)

If you already know the order ID and just need to add a remark:

# Click remark button (ID: addOrderRemark)
remark_btn = driver.ele('css:#addOrderRemark')
anchor = remark_btn.ele('tag:a')
anchor.click()
time.sleep(5)

# Fill textarea
textarea = driver.ele('tag:textarea')
textarea.input(NOTE_TEXT)
time.sleep(1)

# Save — the save button is an <a> tag, not <button>
for a in driver.eles('tag:a'):
    if a.text.strip() == '保存':
        a.click()
        break
time.sleep(3)

Key selectors:

  • Remark trigger: css:#addOrderRemark → inner <a>
  • Remark input: tag:textarea
  • Save button: <a> with text 保存 (class br mr5)
  • Success confirmation: green toast 订单备注保存成功

4. Iframe Navigation

ERP content is usually in an iframe. Filter out small utility iframes like date pickers.

active_page = erp_tab
iframes = erp_tab.eles('tag:iframe')
for iframe in iframes:
    src = iframe.attr('src') or ''
    if 'My97' not in src: # Skip My97DatePicker
        active_page = iframe
        break

5. Popup Handling (Crucial)

Tongtool uses layui for popups which often block clicks and throw NoRectError.

  • Member Center Welcome: Click text:我知道了.
  • Version Announcement (ERP): Often blocks the view. Force remove via JS in both main page and iframe:
def nuke_popups(page):
    page.run_js("""
        () => {
            document.querySelectorAll('.layui-layer-shade, .layui-layer, .modal, .overlay').forEach(el => el.remove());
        }
    """)

# Run on both
nuke_popups(erp_tab)
try: nuke_popups(active_page)
except: pass
time.sleep(2)

6. Menu Navigation

Example: Temu Managed Orders.

# Click Order Management
try:
    menu = active_page.ele('text:订单管理')
    if menu: menu.click()
    time.sleep(3)
except: pass

# Click Temu Managed Orders
try:
    temu = active_page.ele('text:TEMU托管订单')
    if temu: temu.click()
    time.sleep(5)
except: pass

Pitfalls

  • Login redirect chain: After clicking login, the browser lands on /check first, then redirects to member.tongtool.com. Do NOT assume immediate success — poll driver.url for member.

  • Captcha image selector: The captcha image class is pic-yzm, not @src^=/api/common/code. Also the login button is button.btn-primary, not text:立即登录.

  • NoRectError on menu items: The left-menu SPA renders asynchronously. If text:等待配货 or text:发货管理 throws NoRectError, it means the element exists in DOM but hasn't received layout yet. Prefer direct URL navigation over menu clicking.

  • Order detail buttons are <a> tags: In the order detail panel, action buttons (备注, 保存, 取消) are <a> elements, not <button>. Search tag:a when looking for clickable actions.

  • Remark button ID: The "备注" button has a fixed DOM id #addOrderRemark. Text-based search (text:备注) often fails in the SPA because the button text is deeply nested. Always use the ID.

  • Custom t_combo_box warehouse dropdown: The warehouse selector on the wait stock page (assignStockSelfMaintenance/index.htm) is a custom component, not a native <select>. It cannot be clicked open with standard methods. Use the hidden input[name="warehouseSelect"] + change event dispatch, then click the <td> containing the warehouse name. See Section 3.2.

  • Extract order IDs from page source, not DOM: The order grid on the wait stock page is rendered by a custom SPA component. DOM queries for row cells often return stale or empty results. Instead, extract order IDs from the raw driver.html or driver.ele('tag:body').text using regex (e.g., [A-Za-z0-9]+-PO-\d+-\d+). The data is always present in the initial HTML payload even if the visual grid hasn't finished rendering.

  • TEMU 等待配货 orders already have shipping calculated: Do NOT waste time looking for a "运费试算" button. For TEMU semi-managed orders in "等待配货" status, the cheapest shipping method is already assigned and visible in the detail panel under 仓储物流 after clicking the order row. Just read it and record it in the remark.

  • TEMU Platform Interface Price vs System Estimate (CRITICAL): The system logistics panel shows a default shipping estimate (e.g., EJET-USPS(美东) CNY 9.22 / 55g), but this is NOT the price to use for remarks. The correct price must come from the popup "邮寄方式分析" → "TEMU平台接口试算" which shows the authoritative TEMU platform price (e.g., USPS Ground Advantage $2.96). The platform API price is already in USD — no currency conversion is required. Extracting CNY from the logistics panel and dividing by 6.8232 will give the WRONG result. Example: order 88NewGrou-PO-211-19966059489911344 shows CNY 25.20 in the logistics panel (which converts to ~$3.69 at 6.8232), but the TEMU platform API price is actually $2.96. Always use the popup price.

  • Triggering the "邮寄方式分析" popup: The popup URL pattern is search/order.htm?...#showOrderWarehouse{数字ID}. The trigger button (likely a small icon or link in the order detail page) is not discoverable via DOM automation — searching the full HTML source returns zero matches for 邮寄方式分析. If the user provides the platform API price from a screenshot, use that directly instead of attempting to automate the popup.

  • Official exchange rate (fallback only): The Tongtool ERP exchange rate at 基础设置 > 汇率 is 1 USD = 6.8232 CNY (updated 2026-04-27 20:28). Only use this if you MUST convert a CNY price to USD from a non-TEMU source. For TEMU platform API prices, the popup already returns USD.

  • Popup Handling (Crucial): Tongtool uses layui for popups which often block clicks and throw NoRectError.

    • Member Center Welcome: Click text:我知道了.
    • Version Announcement (ERP): Often blocks the view. Force remove via JS in both main page and iframe:
    def nuke_popups(page):
        page.run_js("""
            () => {
                document.querySelectorAll('.layui-layer-shade, .layui-layer, .modal, .overlay').forEach(el => el.remove());
            }
        """)
    
    # Run on both
    nuke_popups(erp_tab)
    try: nuke_popups(active_page)
    except: pass
    time.sleep(2)
    
  • Tab Switching: Don't rely on driver.url. Use CDP Target.getTargets to find the yijiety tab. If that fails, direct navigation to yijiety.tongtool.com works with session cookies.

  • Iframe Context: If you can't find an element, ensure you are looking in active_page (the iframe) and not erp_tab (the main page).

  • Missing Modules: Some accounts may not have certain modules (e.g., "订单管理") visible. This means the module is either not activated or not assigned to this user's role. Check with the account manager or super-admin for permissions.

  • Automation method preference: Use CDP Desktop via Bridge when the target Desktop is online and you want to avoid device-login-verification or captcha. Use DrissionPage headless for batch/automated workflows when the Desktop is offline and the account isn't blocked by device verification. CDP Desktop login is proven to skip captcha entirely on passport.tongtool.com (2026-06-06).

  • CDP evaluate response structure: Always access via r['result']['result']['value'] — Bridge returns {ok: true, result: {result: {type: "...", value: ...}}}. A flat r.get('result') will miss the nested value.

  • Native value setter for React/Vue inputs: Use Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set.call(inp, 'val') instead of element.value = 'val' to trigger framework-controlled value binding.

  • Login button text is "立即登录" on passport.tongtool.com (not button.btn-primary). Use text-based click via document tree walker in CDP evaluate.

  • CDP session staleness: If /cdp/evaluate returns errors, trigger /cdp/snapshot first to re-establish the CDP session.

  • DrissionPage v4 API: Use page.get_tab(i) to get tab objects, page.tabs_count for count. Do NOT use page.tabs (doesn't exist in v4). Use auto_port() for random port assignment.

  • DOM Context: After switching to a tab obtained via get_tab(), call page.refresh() to properly attach the DOM context before querying elements.

  • Device Login Request (NEW — Critical) Tongtool has added a device login verification (设备登录请求) that blocks automated logins.

Detection

After submitting login credentials, if the page URL stays on /check and the HTML contains:

  • <link href=".../deviceLoginRequest.css">
  • Text "设备登录请求" (Device Login Request)
  • A popup with "我知道了" button

Then the account requires manual device approval from the Tongtool admin console or a mobile device.

Behavior

  • This triggers even with Playwright + real browser headers
  • requests/DrissionPage/Playwright — all blocked equally
  • The page loads deviceLoginRequest.css and shows a waiting/approval UI

Workaround / Recovery

  1. Ask the user to approve the device in the Tongtool admin console (or check their phone for an approval notification).
  2. Alternative: Use a previously approved session. If you have fresh cookies from a browser where the device is already approved, navigate directly to https://yijiety.tongtool.com/dashboard/homepage/index.htm — the session may still be valid.
  3. Cookie freshness: The JSESSIONID and ttcuid cookies expire quickly when device verification is enforced. Saved cookies from /home/ubuntu/.hermes/cookies/tongtool_cookies.json often fail after ~24 hours.

Verification script

# After login POST, check for device verification
if 'deviceLoginRequest' in login_resp.text or '设备登录请求' in login_resp.text:
    print("🚨 Device login verification required — cannot proceed automatically")
    print("Please approve this device in the Tongtool admin console.")
    return False