diff --git a/skills/archived/tongtool-temu-shipping-calc/SKILL.md b/skills/archived/tongtool-temu-shipping-calc/SKILL.md new file mode 100644 index 0000000..e52072b --- /dev/null +++ b/skills/archived/tongtool-temu-shipping-calc/SKILL.md @@ -0,0 +1,559 @@ +--- +name: tongtool-temu-shipping-calc +description: Playwright automation for Tongtool ERP TEMU semi-managed orders — filter pre-print orders WITHOUT existing remarks, login with captcha retry, iterate through each order, open 仓储物流 → 邮寄方式解析 → TEMU平台接口试算, click 查询, find cheapest shipping price, write remark to order (or error remark on failure), and support scheduled looping. +--- + +# Tongtool TEMU Shipping Cost Calculator (v2) + +End-to-end automation: **find pre-print (未完成通途打印) TEMU half-managed orders without remarks** → compute cheapest TEMU API shipping price → write remark back to order → repeat on schedule. + +## Core Requirements (User Spec) + +1. **筛选目标**:未完成通途打印步骤 **且** 没有备注 的 TEMU 半托管订单 +2. **逐个处理**:订单详情 → 仓储物流 → 邮寄方式解析 → **TEMU平台接口试算**(⚠️ 不是运费模板试算)→ 点击查询 → 找出最低价 +3. **写入备注**: + - 成功:格式如 `GOFO,Standard 预估$2.50` + - 失败:写入 `出错没找到最低价` +4. **定时循环**:支持 cron/systemd 定时重复执行 + +## Prerequisites +- `playwright` + Chromium at `/home/ubuntu/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome` +- `ddddocr` for captcha OCR +- `requests` for captcha image download +- Tongtool account: `ozonezsjgw@163.com` / `111111` + +## 1. Login with Captcha Retry + +**Critical fix**: `ddddocr` may return CJK symbols/non-printable chars. Filter to ASCII alphanumeric before character substitution. + +```python +import asyncio, re, subprocess, requests +from playwright.async_api import async_playwright + +USERNAME = 'ozonezsjgw@163.com' +PASSWORD = '111111' +BROWSER_PATH = '/home/ubuntu/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome' + +def correct_ocr(raw): + cleaned = ''.join(c for c in raw if c.isascii() and c.isalnum()) + return cleaned.translate(str.maketrans('oliOSsZz', '01105522')) + +async def do_login(page, context): + await page.goto('https://passport.tongtool.com/') + await asyncio.sleep(3) + await page.fill('input[name="username"]', USERNAME) + await page.fill('input[name="password"]', PASSWORD) + + for attempt in range(5): + img = page.locator('img#captcha') + src = await img.get_attribute('src') + if src.startswith('/'): + src = 'https://passport.tongtool.com' + src + cookies = await context.cookies() + cookie_dict = {c['name']: c['value'] for c in cookies} + resp = requests.get(src, cookies=cookie_dict, timeout=10) + raw = subprocess.check_output(['python3', '-c', f""" +import ddddocr +print(ddddocr.DdddOcr(show_ad=False).classification({resp.content!r})) +"""]).decode().strip() + code = correct_ocr(raw) + if len(code) < 4: + await img.click() + await asyncio.sleep(2) + continue + await page.fill('input[name="captcha"]', code) + await page.click('button.btn-primary') + await asyncio.sleep(8) + if 'member' in page.url: + return True + return False +``` + +## 2. Navigate + Select Warehouse + +```python +async def goto_wait_stock(page): + await page.goto('https://yijiety.tongtool.com/warehouse/assignStockSelfMaintenance/index.htm') + await asyncio.sleep(30) + + # Open warehouse dropdown + await page.evaluate(""" + var spans = document.querySelectorAll('span'); + for (var s of spans) { + if (s.textContent.trim() === '请选择仓库:') { + s.parentElement.click(); break; + } + } + """) + await asyncio.sleep(3) + await page.evaluate("document.querySelector('td[title=\"武汉仓库\"]').click()") + await asyncio.sleep(15) + + # Click 查询 + await page.evaluate(""" + var all = document.querySelectorAll('a, button, span'); + for (var i=0; i { + var rows = document.querySelectorAll('tr, div.grid-row, div.x-grid3-row'); + var results = []; + for (var row of rows) { + var orderNo = ''; + // Remove whitespace from textContent to prevent regex breaking on visually wrapped long IDs + for (var cell of row.querySelectorAll('td, div.cell')) { + var txt = (cell.textContent || '').replace(/\\s+/g, ''); + var m = txt.match(/([A-Za-z0-9]+-PO-\\d+-\\d+)/i); + if (m) { orderNo = m[1]; break; } + } + if (!orderNo) continue; + + // Idempotency: skip if already processed + var rowText = row.textContent || ''; + var hasRemark = rowText.includes('[TEMU试算]') || rowText.includes('出错没找到最低价'); + if (hasRemark) continue; + + results.push(orderNo); + } + return results; + }""") + # Deduplicate + seen = set() + unique = [] + for o in targets: + if o not in seen: + seen.add(o) + unique.append(o) + print(f'Found {len(unique)} pre-print, un-remarked TEMU half-managed orders') + return unique +``` + +> **Note**: Exact selectors for order status and remark columns may vary. If the default logic misses, use DevTools to find the specific column class/index and adjust the `rowText` check to read targeted `tds[idx]` instead. + +## 4. Open 邮寄方式解析 Popup (per order) + +**Critical**: 编辑 button requires exact text match `=== '编辑'` + visibility check. `includes('编辑')` matches invisible nodes. + +```python +async def open_shipping_popup(page, order_no): + # Close residual popups + await page.evaluate(""" + var closeBtns = document.querySelectorAll('.jclose-blue, .close, [title="关闭"], .layui-layer-close'); + for (var i=0; i {{ + var links = document.querySelectorAll('a'); + for (var i=0; i { + var all = document.querySelectorAll('*'); + for (var i=0; i 0) { + all[i].click(); return; + } + } + }""") + await asyncio.sleep(8) + + # Click 编辑 — exact match + visibility + await page.evaluate("""() => { + var all = document.querySelectorAll('a, button, span'); + for (var i=0; i 0 && rect.height > 0) { + all[i].click(); return; + } + } + } + }""") + await asyncio.sleep(8) + + # Click 邮寄方式解析 + opened = await page.evaluate("""() => { + var all = document.querySelectorAll('a'); + for (var i=0; i` +- **Must click `TEMU平台接口试算` tab** (NOT `运费模板试算`) +- After switching tab, **must click 查询** button inside `#temuShippingMethod` to trigger API call +- Prices come back as e.g. `GOFO` (carrier) / `Standard` (method) / `$2.50` (USD price) + +```python +async def extract_temu_cheapest(page): + """Returns string like 'GOFO,Standard 预估$2.50' or None on failure.""" + # Step 1: Switch to TEMU tab + switched = await page.evaluate("""() => { + var popups = document.querySelectorAll('div[windowid]'); + var popup = null; + for (var p of popups) { + if (p.querySelector('#tabpanelDiv')) { popup = p; break; } + } + if (!popup) return false; + var tab = popup.querySelector('li[tabtitle="TEMU平台接口试算"]'); + if (tab) { tab.click(); return true; } + return false; + }""") + if not switched: + return None + await asyncio.sleep(10) + + # Step 2: Click 查询 button inside #temuShippingMethod + await page.evaluate("""() => { + var popups = document.querySelectorAll('div[windowid]'); + var popup = null; + for (var p of popups) { + if (p.querySelector('#tabpanelDiv')) { popup = p; break; } + } + if (!popup) return; + var temuDiv = popup.querySelector('#temuShippingMethod'); + if (temuDiv && !temuDiv.classList.contains('hide')) { + var btns = temuDiv.querySelectorAll('a, button'); + for (var b of btns) { + if (b.textContent.includes('查询') || b.textContent.includes('试算')) { + b.click(); return; + } + } + } + }""") + await asyncio.sleep(12) + + # Step 3: Extract all carrier/method/price triples and find cheapest + prices = await page.evaluate("""() => { + var popups = document.querySelectorAll('div[windowid]'); + var popup = null; + for (var p of popups) { + if (p.querySelector('#tabpanelDiv')) { popup = p; break; } + } + if (!popup) return []; + var temuDiv = popup.querySelector('#temuShippingMethod'); + if (!temuDiv) return []; + + var results = []; + var rows = temuDiv.querySelectorAll('tr'); + for (var row of rows) { + var tds = row.querySelectorAll('td'); + if (tds.length < 3) continue; + // Try to find USD price ($x.xx) in the row + var rowText = row.textContent; + var currency = '$'; + var priceMatch = rowText.match(/\\$\\s*(\\d+\\.\\d+)/); + if (!priceMatch) { + priceMatch = rowText.match(/[¥¥]\\s*(\\d+\\.\\d+)/); + currency = '¥'; + } + if (!priceMatch) continue; + + // Skip disabled options ("未启用") entirely + if (rowText.includes('未启用')) continue; + + // carrier usually in td[1] or td[0], method in td[2] + var carrier = '', method = ''; + for (var td of tds) { + var t = td.textContent.trim(); + if (!t || t === priceMatch[0]) continue; + if (!carrier) { carrier = t; continue; } + if (!method) { method = t; break; } + } + if (!carrier && !method) continue; + results.push({ + carrier: carrier, + method: method, + currency: currency, + price: parseFloat(priceMatch[1]) + }); + } + return results; + }""") + + if not prices: + return None + cheapest = min(prices, key=lambda x: x['price']) + + # Format: 'GOFO,Standard 预估$2.50' + # Fallback to handle missing carrier + c = (cheapest.get('carrier') or '').strip() + m = (cheapest.get('method') or '').strip() + label = f"{c}>>{m}" if c and m else (c or m) + return f"{label} {cheapest['currency']}{cheapest['price']:.2f}" +``` + +## 6. Add Order Remark (success OR error) + +```python +async def add_remark(page, remark_text): + """Add remark text (success price OR error message) to the open order.""" + clicked = await page.evaluate("""() => { + var btn = document.querySelector('#addOrderRemark'); + if (btn) { + var a = btn.querySelector('a'); + if (a) { a.click(); return true; } + } + return false; + }""") + if not clicked: + return False + await asyncio.sleep(5) + + # Fill textarea (escape single quotes in remark_text) + escaped = remark_text.replace("'", "\\'").replace('"', '\\"') + await page.evaluate(f"""() => {{ + var ta = document.querySelector('textarea'); + if (ta) {{ + ta.value = '{escaped}'; + ta.dispatchEvent(new Event('input', {{bubbles:true}})); + ta.dispatchEvent(new Event('change', {{bubbles:true}})); + }} + }}""") + await asyncio.sleep(1) + + # Click 保存 + saved = await page.evaluate("""() => { + var links = document.querySelectorAll('a, button'); + for (var i=0; i 0`. Naive `includes('编辑')` hits invisible DOM. + +7. **Popup is `div[windowid]`, not iframe**: Random `windowid` attribute, lives in main page DOM. Search by `#tabpanelDiv` structure, not by hardcoded ID. + +8. **查询 button inside #temuShippingMethod must be clicked after tab switch**: Tab switch alone doesn't trigger API call. + +9. **Dispatch input/change events after setting textarea value**: React/Vue components won't detect `.value = ...` assignment without events. Use `dispatchEvent(new Event('input'))`. + +10. **Warehouse selection**: dropdown is custom component — click label parent, then `td[title="武汉仓库"]`. + +11. **Generous waits between actions**: SPA uses ExtJS-style rendering; use 8–15s between clicks for reliable popup/tab rendering. + +12. **Session 2026-04-28 test**: Successfully logged in and processed 2 TEMU half-managed orders out of 50 total orders. + +13. **2026-04-28 单轮实测(limit=3)**: 登录一次过(captcha='35cn2'),目标订单 1 个 `ALuckhalf-PO-211-17213720356471367`,成功写入备注 `[TEMU试算]USPS,Ground Advantage $3.32`(原始文本 `预估$3.32; 2-5 个工作日送达`)。脚本:`/home/ubuntu/scripts/tongtool_temu_once.py`(用法 `python3 scripts/tongtool_temu_once.py [limit]`)。 + +14. **carrier 列可能为空**: 某些 TEMU 平台返回的表只有 method 列而 carrier 列空,格式化备注时要做兜底 `label = f"{c}>>{m}" if c and m else (c or m)`,否则备注变成 `>>USPS,...` 这种首字段空的难看格式。 + +15. **避免 ddddocr subprocess 启动**: 用 subprocess 把图片字节 `{resp.content!r}` 作为参数拼进 python -c 命令会传超大二进制,极易失败。应在主进程直接 `import ddddocr; ocr.classification(bytes)`。venv `/home/ubuntu/.hermes/hermes-agent/venv/bin/python3` 已装 ddddocr。 + +16. **运行结果持久化**: 每轮输出 `/home/ubuntu/tongtool_temu_runs/run_.json`,便于事后审计和诊断。 + +17. **Order Number Extraction**: Use `textContent` and `.replace(/\s+/g, '')` instead of `innerText`. Tongtool inserts visual line breaks in long order numbers (e.g., `2ALuckhalf...726`), which breaks regex matching if whitespace isn't stripped. + +18. **Order Number Pattern**: TEMU half-managed orders do NOT always contain the word `half` (e.g., `88NewGrou-PO-...`). Use `([A-Za-z0-9]+-PO-\d+-\d+)` to match all of them. + +19. **Status Filter**: Removed DOM-based status checks per user preference. Rely purely on idempotency (checking for `[TEMU试算]` or `出错没找到最低价`) to determine if an order needs processing. + +20. **Tongtool 订单状态生命周期**: 订单状态依次流转为:`订单生成` → `付款` → `检查` → `配货` → `物流商下单` → `通途打印` → `通途发货` → `物流商发货` → `签收`。筛选时,若要处理发货前的订单,需排查包含 `通途打印` 及后续状态的关键字,而不能仅依赖 `已发货`,因为“通途打印”标志着仓库作业已实质开始。 + +21. **⚠️ PASSWORD 被占位符替换导致登录失败**: 脚本文件(如 `tongtool_temu_once_v2.py`)中的 `PASSWORD='***'` 是脱敏占位符,实际运行时会被拒绝。若 OCR 返回的验证码看起来正常(如 `g72fg`、`55m38d`)但登录反复失败,先用 `grep -n "PASSWORD"` 检查是否为 `'***'` 或 `'111111'`。可通过以下命令批量检查目录下所有脚本:`grep -rn "PASSWORD.*='\\*\\*\\*'" /home/ubuntu/scripts/`。 + +22. **浏览器启动参数需加 `ignore_https_errors=True`**: `context = await browser.new_context(ignore_https_errors=True)` 避免 Tongtool 子域(如 `yijiety.tongtool.com`)的证书问题阻断页面加载。 + +23. **`goto_wait_stock` 需等待 30 秒**: 待配货页 (`assignStockSelfMaintenance/index.htm`) 是重度 SPA,首次加载后 ExtJS 组件渲染耗时约 20-30 秒。`await asyncio.sleep(30)` 后再操作仓库下拉框,否则选择器可能匹配不到 DOM。 + +24. **单脚本文件路径**: 当前稳定版本为 `/home/ubuntu/scripts/tongtool_temu_once_v2.py`,用法:`python3 scripts/tongtool_temu_once_v2.py [limit]`。运行结果自动写入 `/home/ubuntu/tongtool_temu_runs/run_.json`。 + +25. **"未启用" (Disabled) Shipping Options**: TEMU API often returns shipping methods that are disabled ("未启用") and completely lack carrier/method text, causing them to be parsed with empty labels (e.g., `>>undefined`) but sometimes containing valid prices. The DOM scraper must explicitly check `rowText.includes('未启用')` and skip the row, and also skip saving entries where both `carrier` and `method` are empty. +26. **OpenAPI Pagination Bypass**: To process batches of orders without getting stuck on paginated UI containing already-handled orders, query `open.tongtool.com/apiv3-service/openapi/tongtool/ordersQuery` (using `merchantId`, `pageNo`, and `pageSize`) to get a clean list of orders first (filtering out ones with `[TEMU试算]` in remark), sort them by the last 4 digits of order ID, and feed this target list to the UI automation script. \ No newline at end of file