20 KiB
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(nottext:立即登录). - After submit, URL lands on
/checkthen redirects tomember.tongtool.com— wait formemberin 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:
- Navigate:
POST /cdp/navigate->https://passport.tongtool.com/ - Fill username via native setter:
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set.call(inp, 'ozonezsjgw@163.com') - Fill password via native setter
- Click login button by text
立即登录 - Wait for redirect:
member.tongtool.com - 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.96;2-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)
3.6 Add order remark (single order via direct search)
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保存(classbr 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
/checkfirst, then redirects tomember.tongtool.com. Do NOT assume immediate success — polldriver.urlformember. -
Captcha image selector: The captcha image class is
pic-yzm, not@src^=/api/common/code. Also the login button isbutton.btn-primary, nottext:立即登录. -
NoRectError on menu items: The left-menu SPA renders asynchronously. If
text:等待配货ortext:发货管理throwsNoRectError, 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>. Searchtag:awhen 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_boxwarehouse 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 hiddeninput[name="warehouseSelect"]+changeevent 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.htmlordriver.ele('tag:body').textusing 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: order88NewGrou-PO-211-19966059489911344showsCNY 25.20in 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
layuifor popups which often block clicks and throwNoRectError.- 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) - Member Center Welcome: Click
-
Tab Switching: Don't rely on
driver.url. Use CDPTarget.getTargetsto find theyijietytab. If that fails, direct navigation toyijiety.tongtool.comworks with session cookies. -
Iframe Context: If you can't find an element, ensure you are looking in
active_page(the iframe) and noterp_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 flatr.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 ofelement.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/evaluatereturns errors, trigger/cdp/snapshotfirst to re-establish the CDP session. -
DrissionPage v4 API: Use
page.get_tab(i)to get tab objects,page.tabs_countfor count. Do NOT usepage.tabs(doesn't exist in v4). Useauto_port()for random port assignment. -
DOM Context: After switching to a tab obtained via
get_tab(), callpage.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.cssand shows a waiting/approval UI
Workaround / Recovery
- Ask the user to approve the device in the Tongtool admin console (or check their phone for an approval notification).
- 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. - Cookie freshness: The
JSESSIONIDandttcuidcookies expire quickly when device verification is enforced. Saved cookies from/home/ubuntu/.hermes/cookies/tongtool_cookies.jsonoften 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