Add archived/tongtool-erp-automation
This commit is contained in:
@@ -0,0 +1,423 @@
|
|||||||
|
---
|
||||||
|
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.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:
|
||||||
|
```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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user