Files
atomk-hermes-skills/skills/archived/miaoshou-collect-box/SKILL.md
T

370 lines
22 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: miaoshou-collect-box
category: cross-border-ecommerce
description: Explore and interact with Miaoshou ERP's "通用采集箱" (Common Collect Box) — URLs, collection methods, import paths, and login automation.
---
# 妙手ERP 通用采集箱 (Common Collect Box)
## Overview
Miaoshou ERP (`erp.91miaoshou.com`) has a **Common Collect Box** (`通用采集箱`) at a fixed URL. This skill documents how to navigate to it, the collection methods available, and how to interact with the import/collection UI programmatically via DrissionPage.
## Login
- URL: `https://erp.91miaoshou.com/auth/login`
- Credentials: `xiaochaoren2026 / [REDACTED]`
- CAPTCHA: **Base64 inline image** (`data:image/png;base64,...`) rendered in `.captcha-img`
- Unlike Dianxiaomi (which uses a separate PNG endpoint), Miaoshou embeds the captcha directly.
- Extract with JS: `document.querySelector('.captcha-img').src`
- Decode base64, save to file, run `ddddocr.classification()`
- Apply digit cleaning: `raw.translate(str.maketrans('oliOSsZz', '01105522'))`
- Form selectors:
- Account: `css:.account-input`
- Password: `css:.password-input`
- Captcha text: `css:.captcha-text`
- Captcha UUID: `input[name="captchaUuid"]` (must be set/synced)
- Submit: `css:.login-button`
- Success redirects to: `https://erp.91miaoshou.com/welcome`
- Cookie file: `~/.hermes/cookies/miaoshou_cookies.json` (autoLoginToken is critical)
## Common Collect Box URL
Direct link (works after login):
```
https://erp.91miaoshou.com/common_collect_box/index
```
Title: `妙手-产品采集`
## Collection Methods Discovered
The page exposes **6 collection methods** as tab-like options:
| Method | Chinese Name | Notes |
|--------|-------------|-------|
| Link Collection | 链接采集 | Enter public product URLs (1688, Taobao, etc.). System scrapes the page. **Requires publicly accessible pages** — internal systems (like AtomK behind login) will NOT work. |
| Import Collection | 导入采集 | **Bulk import via file upload** (template-based). This is the viable path for transferring data from AtomK or other internal systems. |
| 1688 AI Selection | 1688Ai选品 | AI-assisted product selection from 1688 |
| Inventory Collection | 货盘采集 | Collect from inventory/supplier pallets |
| Keyword Collection | 关键词采集 | Search keyword-based batch collection |
| Whole-Store Collection | 整店采集 | Enter a store URL to collect all products |
| Plugin Collection | 插件采集 | Browser extension-based collection |
| Store Cross-Collection | 店铺互采 | Cross-store collection |
## Import Collection (导入采集) — 4 Sub-Methods
When you click "导入采集", the page switches to `?fetchType=importCopy` and shows **4 sub-tabs**:
| Sub-Method | Chinese Name | What It Does |
|-----------|-------------|-------------|
| Import Link Collection | 导入链接采集 | Upload an **XLS file** with product URLs. Miaoshou scrapes each URL. |
| Excel Spreadsheet Import | Excel表格导入 | Upload a pre-structured Excel with full product data (details TBD). |
| Local Material Package | 本地素材包导入 | Upload a local asset package (images + data). |
| Jushuitan Material Package | 聚水潭素材包导入 | Import from Jushuitan ERP format. |
### Import Link Collection Template (导入链接采集)
This is the **most important** one for AtomK-style workflows.
**Template file**: `导入产品链接模板.xls` (downloadable via "下载导入模板" link in the modal)
**File format**: `.xls` (older Excel 97-2003, NOT xlsx) — verified April 2026. `.xlsx` triggers `解析文件数据错误`.
**CRITICAL**: The template is **strictly 5 columns**. Any extra column causes failure.
| Column | Field | Required | Notes |
|--------|-------|----------|-------|
| A | 链接地址(必填) | ✅ Yes | Product detail page URL. **Must be publicly accessible**. Login-gated pages (like AtomK behind login) will fail. |
| B | 产品标题 | No | **AI titles ARE preserved** — verified. Overrides scraped 1688 title. |
| C | 价格(RMB | No | **AI prices ARE preserved** — verified. Overrides scraped 1688 price. |
| D | 促销价(RMB | No | Only used when publishing to **Lazada**. |
| E | 提示:促销价仅可用于采集到Lazada | No | Literal hint text — include this column to match the official template exactly. |
**Empirically verified findings (April 2026)**:
- ✅ 5-column `.xls` with exact headers above → `解析成功 N/0/N`
- ❌ 6+ columns (e.g., adding "产品描述") → `导入文件未识别到:【产品主编号】表头`
- ❌ Adding "产品主编号" column → `解析文件数据错误`
-`.xlsx` format → `解析文件数据错误`
- ✅ AI title from XLS **overrides** scraped 1688 title in the final product
- ✅ AI price from XLS **overrides** scraped 1688 price
- ✅ Sheet name can be `Worksheet` or `Sheet1` (doesn't matter with correct 5-col headers)
- ✅ Encoding can be `utf_16_le` or `utf-8` (doesn't matter with correct 5-col headers)
**Important**: The error *"导入文件未识别到:【产品主编号】表头"* is a **misleading** catch-all for "your columns don't match the expected 5-column template". Do NOT add a "产品主编号" column — doing so triggers `解析文件数据错误`. Always use the exact 5-column format. The system does not actually use or store a "产品主编号" from Excel; SKU must be filled later via the edit dialog.
**Critical limitation**: The "导入链接采集" method fundamentally relies on **web scraping** each product URL. If the URL requires authentication (e.g., AtomK product pages behind `atomlisting.com` login), Miaoshou cannot fetch the data. The 5-column XLS only passes the link + AI title + AI price; Miaoshou still scrapes the 1688 page for images, description, specs, etc.
#### File Upload Mechanics
When the import modal is open:
- The upload area is a hidden `<input type="file" class="jx-upload__input">` inside a `.pro-upload` / `.jx-upload--text` container.
- **Selector**: `css:input[type=file]` or `css:.jx-upload__input`
- **Accepts**: `.xls,.xlsx,.csv` (but only `.xls` works reliably)
- Use `file_input.input('/path/to/file.xls')` to set the file path programmatically.
- After upload, the file name appears in `.jx-upload-list li`.
- **The confirm button is inside the modal dialog** — search within `.jx-dialog` for buttons with text `确认`.
- **After clicking confirm, the modal closes and the file appears in the "已导入的文件列表" table.** Wait ~15s for parsing, then **refresh the page** to check status.
- **The page does NOT auto-update after confirm.** You must navigate back to `?fetchType=importCopy` or use `page.get()` to refresh and see the parse result.
#### Complete Upload Flow (Verified April 2026)
```python
# 1. Navigate to import page
page.get("https://erp.91miaoshou.com/common_collect_box/index?fetchType=importCopy")
time.sleep(10)
# 2. Click Excel import button
page.run_js("""
var all = document.querySelectorAll('span, div, button, a');
for (var i=0; i<all.length; i++) {
var txt = all[i].innerText || '';
if (txt.includes('Excel表格导入') && txt.length < 20) {
all[i].click(); break;
}
}
""")
time.sleep(5)
# 3. Upload file
file_inputs = page.eles('css:input[type="file"]')
file_inputs[0].input('/path/to/import.xls')
time.sleep(5)
# 4. Click confirm
page.run_js("""
var btns = document.querySelectorAll('button');
for (var i=0; i<btns.length; i++) {
if (btns[i].innerText && btns[i].innerText.trim() === '确认') {
btns[i].click(); break;
}
}
""")
# 5. Wait for backend parsing, then refresh to check result
time.sleep(20)
page.get("https://erp.91miaoshou.com/common_collect_box/index?fetchType=importCopy")
time.sleep(10)
# 6. Check result in page body text
body_text = page.run_js("return document.body.innerText;")
# Look for: '解析成功' or '解析失败' near the filename
```
#### Known Error Patterns
| Error Message | Meaning | Fix |
|---------------|---------|-----|
| `导入文件未识别到:【产品主编号】表头` | Column count ≠ 5 or headers don't match | Ensure **exactly 5 columns** with headers: `链接地址(必填)`, `产品标题`, `价格(RMB`, `促销价(RMB`, `提示:促销价仅可用于采集到Lazada`. Do NOT add "产品主编号" — it's a misleading error. |
| `解析文件数据错误` | Wrong format or added "产品主编号" | Use `.xls` (not `.xlsx`). Remove any "产品主编号" column. Ensure exact 5-column headers. |
| `解析失败` (with `0/N/N`) | URLs accepted but scraping failed | Linked pages are unsupported (e.g., COS-hosted custom HTML, login-gated pages). Use standard 1688/Taobao/Pinduoduo URLs. |
## Collect Box Product Edit Dialog (采集箱产品编辑弹窗)
After a product is imported and **claimed to a platform** (e.g., Temu全托管), it appears in the **已认领** tab. Each row has an **编辑** button that opens a modal dialog for editing the product's full data.
This dialog is the key to filling AI-generated fields (SKU, description, keywords) that cannot be imported via the 5-column XLS template.
### Dialog Structure
- **Root dialog**: `.jx-dialog.pro-dialog.collect-box-editor-dialog-V2` (1880×953)
- **Editor body**: `.collect-box-editor` (1559×953)
- **Tabs**: 基本信息, 产品属性, 销售属性, 产品图片, 产品视频, 认证说明, 物流信息, 货源链接
### Critical Field Mappings (Verified April 2026)
The editor uses Vue.js with custom `jx-input` components. Direct `.value` assignment works but **must be followed by event dispatching** for Vue to sync state.
| Field | DOM Element | Selector Strategy | Notes |
|-------|------------|-------------------|-------|
| 产品标题 | `input[type="text"]` (multiple) | Find first input whose `.value` contains the title text | Title is pre-filled from import; usually does NOT need updating. |
| **产品主编号 (SKU)** | `input[type="text"]` at **index 1** | `editor.querySelectorAll('input')[1]` | Placeholder: `请输入产品主编号,不填将自动生成`. This is the **most reliable** field to target. |
| **简易描述** | `textarea` at **index 0** | `editor.querySelectorAll('textarea')[0]` | Plain text. Fills to Shopee/Wish/Joom/Mercado Libre/Ozon/Walmart product description. |
| **详细描述** | `iframe` at **index 0** (TinyMCE-like) | `editor.querySelectorAll('iframe')[0]``contentDocument.body` | Rich text editor. Used for Lazada/速卖通/TikTok/Coupang/eBay/Allegro. Body is `contenteditable`. |
### Vue State Sync (Mandatory)
Miaoshou uses Vue.js. Simply setting `.value` is **not enough** — the change must be propagated to Vue's reactive state. After setting any value, dispatch:
```javascript
el.value = 'your-value';
el.dispatchEvent(new Event('input', {bubbles: true}));
el.dispatchEvent(new Event('change', {bubbles: true}));
el.dispatchEvent(new Event('blur', {bubbles: true}));
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'a', bubbles: true}));
el.dispatchEvent(new KeyboardEvent('keyup', {key: 'a', bubbles: true}));
```
Without this, clicking **保存修改** will silently discard your changes.
### Opening the Edit Dialog Programmatically
Products are rendered in a **virtual scroll table** (`.pro-virtual-table__row`), not a standard `<table>`.
```python
# 1. Navigate to claimed tab
page.get('https://erp.91miaoshou.com/common_collect_box/items')
# ... click 已认领 tab ...
# 2. Find the row by collect_box_id and click 编辑
page.run_js("""
var rows = document.querySelectorAll('.pro-virtual-table__row, .pro-virtual-scroll__row');
for (var i=0; i<rows.length; i++) {
var txt = rows[i].innerText || '';
if (txt.includes('3516358992')) { // your collect_box_id
var btns = rows[i].querySelectorAll('button');
for (var j=0; j<btns.length; j++) {
if (btns[j].innerText && btns[j].innerText.trim() === '编辑') {
btns[j].click();
break;
}
}
}
}
""")
time.sleep(10) # Dialog takes time to render
editor = page.ele('css:.collect-box-editor')
```
### Filling Each Field
#### SKU (产品主编号)
```python
editor.run_js(f"""
var editor = document.querySelector('.collect-box-editor');
var skuInput = editor.querySelectorAll('input')[1];
skuInput.value = {json.dumps(sku)};
skuInput.dispatchEvent(new Event('input', {{bubbles: true}}));
skuInput.dispatchEvent(new Event('change', {{bubbles: true}}));
skuInput.dispatchEvent(new Event('blur', {{bubbles: true}}));
skuInput.dispatchEvent(new KeyboardEvent('keydown', {{key: 'a', bubbles: true}}));
skuInput.dispatchEvent(new KeyboardEvent('keyup', {{key: 'a', bubbles: true}}));
""")
```
#### Simple Description (简易描述)
```python
editor.run_js(f"""
var editor = document.querySelector('.collect-box-editor');
var ta = editor.querySelectorAll('textarea')[0];
ta.value = {json.dumps(description)};
ta.dispatchEvent(new Event('input', {{bubbles: true}}));
ta.dispatchEvent(new Event('change', {{bubbles: true}}));
ta.dispatchEvent(new Event('blur', {{bubbles: true}}));
""")
```
#### Rich Description (详细描述 — iframe)
```python
html_content = f'<p><span style="font-size: 12.0pt;">{description.replace(chr(10), "</span></p><p><span style=\\"font-size: 12.0pt;\\">")}</span></p>'
editor.run_js(f"""
var editor = document.querySelector('.collect-box-editor');
var iframe = editor.querySelectorAll('iframe')[0];
var doc = iframe.contentDocument || iframe.contentWindow.document;
var body = doc.querySelector('body');
body.innerHTML = {json.dumps(html_content)};
body.dispatchEvent(new Event('input', {{bubbles: true}}));
""")
```
### Saving
```python
page.run_js("""
var btns = document.querySelectorAll('button');
for (var i=0; i<btns.length; i++) {
if (btns[i].innerText && btns[i].innerText.includes('保存')) {
btns[i].click();
break;
}
}
""")
time.sleep(5)
```
### Verification
After saving, re-open the edit dialog and read back the values to confirm persistence:
```python
verify = editor.run_js("""
var editor = document.querySelector('.collect-box-editor');
var inputs = editor.querySelectorAll('input');
var textareas = editor.querySelectorAll('textarea');
var iframes = editor.querySelectorAll('iframe');
var result = {};
if (inputs.length > 1) result.sku = inputs[1].value;
if (textareas.length > 0) result.desc = textareas[0].value;
for (var i=0; i<iframes.length; i++) {
try {
var doc = iframes[i].contentDocument || iframes[i].contentWindow.document;
var body = doc.querySelector('body');
if (body) { result.rich_desc = body.innerText; break; }
} catch(e) {}
}
return result;
""")
# verify.sku should match, verify.desc should contain AI text
```
### AtomK → Miaoshou Batch Import Script
A production-ready end-to-end script exists at `/home/ubuntu/auto_miaoshou_import.py`:
**What it does:**
1. Logs into AtomK API, fetches claimed products + their AI-generated listings
2. Generates a 5-column `.xls` file (`utf_16_le` encoding, `Worksheet` sheet name)
3. Launches headless Chromium, loads Miaoshou cookies
4. Navigates to `?fetchType=importCopy`, clicks `Excel表格导入`
5. Uploads the `.xls` file, clicks `确认`
6. Refreshes the page after 20s to verify `解析成功`
7. Logs results and updates state file
**State tracking:** `/home/ubuntu/.hermes/miaoshou_import_state.json`
- Tracks already-imported `product_id`s to avoid duplicates
- Updated after each successful run
**Cron job:** `miaoshou-atomk-auto-import` (job_id: `2b6587d6895b`)
- Schedule: `0 * * * *` (every hour on the hour)
- Batch size: 6 products per run
- Logs to: `/home/ubuntu/.hermes/miaoshou_import.log`
**Usage:**
```bash
# Manual run
python3 /home/ubuntu/auto_miaoshou_import.py
# Check cron status
hermes cron list
```
| Scenario | Viable? | Recommended Path |
|---------|---------|-----------------|
| AtomK product pages are **public** | ❌ No | Even **public** AtomK HTML pages (hosted on Tencent COS) fail with `解析失败 0/3`. Miaoshou's scraper is hardcoded for domestic e-commerce platforms (1688, Taobao, Pinduoduo), not custom HTML pages. |
| AtomK product pages require **login** | ❌ No | Same as above — plus authentication barrier. |
| **5-column XLS** import (link + AI title + AI price) | ✅ Yes | `解析成功`. Titles and prices from Excel **override** scraped 1688 data. Verified: AI title `Romantic Heart...` appears instead of 1688 original. |
| 5-column XLS + extra columns (desc, SKU, images, EAN, etc.) | ❌ No | Triggers `导入文件未识别到:【产品主编号】表头`. Adding "产品主编号" triggers `解析文件数据错误`. |
| ".xlsx" format | ❌ No | Triggers `解析文件数据错误`. Must use `.xls` (xlwt). |
| **Full AI data via dual-browser automation** | ✅ **Yes** | **Proven workflow (April 2026)**:<br>1. Import via 5-col XLS (link + AI title + AI price)<br>2. Claim product to target platform<br>3. Open collect box edit dialog<br>4. Auto-fill SKU, 简易描述, 详细描述 (rich text) via DrissionPage + JS<br>5. Save and verify<br>Reference script: `/home/ubuntu/miaoshou_atomk_bridge.py` |
| Direct platform upload (bypass Miaoshou) | ⚠️ Partial | Generate Temu/AliExpress CSV from AtomK API → upload to platform directly. Bypasses Miaoshou entirely but loses Miaoshou's inventory/order management. |
## Page Structure Notes
- Left sidebar contains: `通用功能`, `产品采集`, `公用采集箱`, `AI工作台`, `侵权检测`, `货盘中心`, etc.
- Supported platforms displayed as tags: 1688, 义乌小商品城, 浙宝网, Vvic, 搜款网, 网商园, 货捕头, etc.
- Bottom action bar: `采集并自动认领`, `采集并自动发布`
- A modal/popup may appear on first visit with collection settings — dismiss with "我知道了".
## Browser Setup (DrissionPage)
```python
from DrissionPage import ChromiumPage, ChromiumOptions
co = ChromiumOptions()
co.set_browser_path('/home/ubuntu/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome')
co.set_argument('--headless=new')
co.set_argument('--no-sandbox')
co.set_argument('--disable-dev-shm-usage')
co.set_argument('--window-size=1440,900')
co.set_user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36')
co.set_local_port(get_free_port())
page = ChromiumPage(addr_or_opts=co)
```
## Pitfalls
- **Cookie expiration**: The saved cookie (autoLoginToken) expires relatively quickly. Re-login with captcha is often needed.
- **Base64 captcha session**: Must extract the captcha image from the **same browser session** (via JS `document.querySelector('.captcha-img').src`) — do NOT use `requests` to fetch it separately, or the captcha UUID will mismatch.
- **Tab clicking**: The collection method tabs (链接采集, 导入采集, etc.) are `<span>` elements inside radio-button groups, not standard `<button>` or `<a>` tags. Standard `.ele('xpath://*[contains(text(), "导入采集")]').click()` may fail. Use JS click via `page.run_js()` or search for the actual `<button>` inside the tab component.
- **Import dialog**: Clicking "导入链接采集" opens a modal dialog. The template download link "下载导入模板" triggers a browser download to the default Downloads folder. In headless mode, the file goes to `/home/ubuntu/Downloads/导入产品链接模板.xls`.
- **XLS format only**: The import link template is `.xls` (Excel 97-2003). Using `.xlsx` will likely fail.
- **"暂无当前模块权限"**: Some accounts may see "You don't have permission for this module" if the collect box feature is not enabled for the sub-account. Contact the main account admin to enable.
- **SPA navigation**: Clicking menu items may not trigger full navigation. Use `page.get('https://erp.91miaoshou.com/common_collect_box/index?fetchType=importCopy')` for direct access.
- **Link scraper limitation**: The 导入链接采集 method requires **publicly accessible** product detail pages. Any page behind authentication (AtomK, internal ERPs) will result in empty/failed collection.
## Next Exploration Steps (when needed)
1.~~Click the "导入采集" tab and inspect the file upload input~~ (Done)
2.~~Download the template to understand required columns~~ (Done — 5-column `.xls`: link, title, price, promo_price, hint)
3.~~Analyze "Excel表格导入" template for full-data import~~ (Done — the XLS template is corrupt/unreadable; full-field bulk import is **not viable**. The working path is 5-col import + edit-dialog automation.)
5.~~Automate the file upload and submission~~ (Done — upload via `file_input.input()` + confirm button in modal + page refresh to check result.)
6.~~Automate the edit dialog post-import fill~~ (Done — SKU, 简易描述, 详细描述 all verified saving correctly.)
7.~~Build end-to-end automation~~ (Done — script at `/home/ubuntu/auto_miaoshou_import.py` fetches AtomK API data, generates 5-col XLS, uploads via browser, and tracks state. Cron job `miaoshou-atomk-auto-import` runs hourly.)
8.~~Set up cron job for hourly batch import~~ (Done — job `miaoshou-atomk-auto-import` runs at `0 * * * *`, imports 6 products per run, skips already-imported IDs via `/home/ubuntu/.hermes/miaoshou_import_state.json`.)
9. 🔴 **Future**: Extend edit-dialog automation to fill **产品图片** (replace 1688 images with AI-generated ones from AtomK `platform_fields.pictures`), **产品属性** (material, shape, brand), and **关键词** fields.
10. 🔴 **Future**: Build batch loop that processes multiple `collect_box_id`s end-to-end (import → claim → edit → save).
11. 🔴 **Future**: Investigate if `快速上货` (Quick Listing) offers a more direct API or form for bulk AI data injection, bypassing the collect box entirely.