Files
atomk-hermes-skills/skills/archived/tongtool-erp-automation/SKILL.md
T

423 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: tongtool-erp-automation
description: 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.
```python
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`.
```python
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**:
```python
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.
```python
# 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
```python
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:
```python
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
```python
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:
```python
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
```python
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)
```
### 3.6 Add order remark (single order via direct search)
If you already know the order ID and just need to add a remark:
```python
# 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.
```python
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:
```python
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.
```python
# 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:
```python
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
```python
# 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
```