From 4958eee1ad258e9aa1971f5f67bc4ce6d1d7a387 Mon Sep 17 00:00:00 2001 From: admin9webs Date: Fri, 10 Jul 2026 16:10:43 +0800 Subject: [PATCH] Add archived/atomk-api-export --- skills/archived/atomk-api-export/SKILL.md | 679 ++++++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 skills/archived/atomk-api-export/SKILL.md diff --git a/skills/archived/atomk-api-export/SKILL.md b/skills/archived/atomk-api-export/SKILL.md new file mode 100644 index 0000000..87afe23 --- /dev/null +++ b/skills/archived/atomk-api-export/SKILL.md @@ -0,0 +1,679 @@ +--- +name: atomk-api-export +category: cross-border-ecommerce +description: Extract product and listing data from AtomK via its undocumented REST API (JWT auth), then import into Miaoshou ERP collection box or other platforms. +--- + +# AtomK API Export & Integration Pipeline + +## Overview + +AtomK (`atomlisting.com`) exposes an internal REST API that can be called directly with a JWT Bearer token. This is dramatically more efficient than browser-scraping for bulk export of claimed products and AI-generated listings. + +## Authentication + +The API uses JWT Bearer tokens stored in browser `localStorage` after login. There is a second authentication method specifically for ERP integration, but agent nodes should only use Method 1 (JWT token via login). + +### Extract Token (DrissionPage) +```python +token = page.run_js('return localStorage.getItem("atomk_token");') +headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + 'Accept': 'application/json', +} +``` + +**Token payload fields**: `user_id`, `username`, `email`, `role`, `system_code`, `exp` + +## Complete API Endpoint List (v1 — 2025-05 Upgrade) + +### 认证 Auth +| 方法 | 端点 | 说明 | +|------|------|------| +| POST | /api/v1/auth/login | 登录获取JWT | +| POST | /api/v1/auth/refresh | 刷新token | +| GET | /api/v1/auth/verify-token | 验证token有效性 | +| GET | /api/v1/auth/me | 获取当前用户信息 | + +### 产品 Products +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | /api/v1/products/remote | 远程产品池列表 | +| GET | /api/v1/products/remote/random | 随机获取远程产品 | +| GET | /api/v1/products/remote/by-code/:code | 按产品编码查询 | +| GET | /api/v1/products/remote/categories | 远程产品分类 | +| GET | /api/v1/products/remote/:id | 远程产品详情 | +| POST | /api/v1/products/claim | 认领产品 | +| GET | /api/v1/products/claimed | 已认领产品列表 | +| GET | /api/v1/products/claimed/count | 已认领产品数量 | +| GET | /api/v1/products/claimed/categories | 已认领产品分类 | +| GET | /api/v1/products/claimed/:id | 已认领产品详情 | +| PUT | /api/v1/products/claimed/:id | 更新已认领产品 | +| DELETE | /api/v1/products/claimed/:id | 删除已认领产品 | +| POST | /api/v1/products/images/from-url | 从URL上传图片 | +| POST | /api/v1/products/images/from-file | 从文件上传图片 | +| DELETE | /api/v1/products/images/:id | 删除图片 | + +### Listing 上架 +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | /api/v1/listings | 列表(分页,max 100/page) | +| GET | /api/v1/listings/count | 数量统计 | +| POST | /api/v1/listings | 创建listing | +| GET | /api/v1/listings/:id | 详情 | +| PUT | /api/v1/listings/:id | 更新(含platform_fields) | +| DELETE | /api/v1/listings/:id | 删除 | +| POST | /api/v1/listings/generate | AI生成listing | +| POST | /api/v1/listings/:id/publish | 发布到平台 | + +### 社交帖子 Social +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | /api/v1/social/posts | 社交帖子列表 | +| POST | /api/v1/social/posts | 创建帖子 | +| GET | /api/v1/social/posts/:id | 帖子详情 | +| PUT | /api/v1/social/posts/:id | 更新帖子 | +| DELETE | /api/v1/social/posts/:id | 删除帖子 | +| POST | /api/v1/social/posts/generate | AI生成帖子 | +| POST | /api/v1/social/posts/:id/publish | 发布到社交平台 | +| GET | /api/v1/social/listings | 社交关联listing | + +### 店铺 Stores (16 平台) +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | /api/v1/stores | 所有店铺列表 | +| POST | /api/v1/stores | 创建店铺 | +| GET | /api/v1/stores/:id | 店铺详情 | +| PUT | /api/v1/stores/:id | 更新店铺 | +| DELETE | /api/v1/stores/:id | 删除店铺 | +| POST | /api/v1/stores/:id/test-connection | 测试连接 | +| GET | /api/v1/stores/:id/categories | 店铺分类 | + +### 设置 Settings +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | /api/v1/settings/accounts | 聚合:stores+social+erp+email | +| GET | /api/v1/settings/system-code | 公开,无需认证 | +| GET/POST/PUT/DELETE | /api/v1/settings/social-accounts | 社交账号 CRUD | +| GET/POST/PUT/DELETE | /api/v1/settings/erp-accounts | ERP账号 CRUD(10系统) | +| GET/POST/PUT/DELETE | /api/v1/settings/email-accounts | 邮箱账号 CRUD(10服务商) | + +### 同步 Sync +| 方法 | 端点 | 说明 | +|------|------|------| +| POST | /api/v1/settings/sync/center-to-local | 中心→本地同步(需admin API key) | +| GET | /api/v1/settings/sync/status | 同步状态查询 | + +--- + +## Key Data Structures + +### Claimed Product Fields +| Field | Description | +|-------|-------------| +| `id` | Internal AtomK product ID | +| `remote_product_code` | Product code (e.g., `CNVGX8F4`) | +| `local_sku` | Local SKU | +| `product_name` | English name | +| `product_name_zh` | Chinese name | +| `category` | Category (e.g., `家居百货`) | +| `source_url` | Original 1688/source URL | +| `images` | List of COS image objects | +| `claimed_at` | ISO timestamp | + +**Image structure**: +```python +{ + "id": 8230, + "image_url": "https://9websclub-1251422183.cos.ap-hongkong.myqcloud.com/AK-101/users/4/products/CNVGX8F4/0_0d3a9223.jpeg", + "cos_key": "AK-101/users/4/products/CNVGX8F4/0_0d3a9223.jpeg", + "is_primary": True, + "sort_order": 0 +} +``` + +### Listing Fields +| Field | Description | +|-------|-------------| +| `id` | Listing ID | +| `claimed_product_id` | Links to product `id` | +| `platform` | `temu`, `ozon`, `aliexpress`, etc. | +| `title` | AI-generated title | +| `description` | AI-generated description | +| `price` | Price | +| `currency` | Currency code | +| `keywords` | Comma-separated keywords | +| `status` | `generated` or other | +| `ai_model` | `hunyuan`, `deepseek`, etc. | +| `csv_url` | **Public COS URL** to Temu-standard CSV | +| `html_url` | **Public COS URL** to formatted HTML product page | +| `platform_fields` | Nested dict with SKU, UPC, GTIN, EAN, package dims, image URLs, etc. | +| `created_at` / `updated_at` | ISO timestamps | + +**Platform fields** (Temu example): +```python +{ + "SKU": "RC-MACARON-CANDLES-BOX360", + "ean": "7477596853977", + "upc": "747759886757", + "gtin": "0747759886757", + "price": 10.28, + "price_cny": 69.9, + "quantity": "360", + "exchange_rate": 6.8, + "package_width": "10", + "package_height": "2", + "package_length": "15", + "package_weight": "0.1", + "image_urls": "url1;url2;url3;...", # semicolon-separated COS URLs + "category_path": "Home & Garden > Party Supplies" +} +``` + +### CSV Export (Pre-generated) +The `csv_url` is a **publicly accessible** COS link. No auth required to download: +```python +import requests +csv_resp = requests.get(listing['csv_url'], timeout=30) +# Returns UTF-8 BOM CSV with Temu-standard columns +``` + +### HTML Export (Pre-generated) +The `html_url` is also public. Self-contained, styled HTML product detail page: +```python +html_resp = requests.get(listing['html_url'], timeout=30) +``` + +## Integration with Miaoshou ERP 采集箱 + +### Miaoshou Import Collection Box URL +``` +https://erp.91miaoshou.com/common_collect_box/index?fetchType=importCopy +``` + +### Import Methods +1. **导入链接采集** (Import Link Collection) — requires xls with source URLs +2. **Excel表格导入** (Excel Import) — for bulk data +3. **本地素材包导入** (Local Material Package Import) + +### Import Link Collection Template +Downloaded template: `导入产品链接模板.xls` + +**Columns**: +| Column | Field | Required | +|--------|-------|----------| +| A | 链接地址(必填) | ✅ Yes | +| B | 产品标题 | Optional | +| C | 价格(RMB) | Optional | +| D | 促销价(RMB) | Optional (Lazada only) | + +**How Miaoshou processes it**: For each link, Miaoshou's scraper visits the URL and extracts product info (images, title, description). Optional columns override scraped values. + +### Workflow: AtomK → Miaoshou (Link Import) — Validated ✅ + +**CRITICAL**: Miaoshou's scraper only supports **1688/Taobao/Pinduoduo** URLs. AtomK's `html_url` (COS-hosted HTML) will **always fail** with "解析失败". You must use the **original `source_url`** from `/api/v1/products/claimed`. + +```python +import requests +import xlwt + +requests.packages.urllib3.disable_warnings() + +# 1. Login AtomK +login_resp = requests.post('https://www.atomlisting.com/api/v1/auth/login', + json={'username': 'admincao', 'password': 'Tt123456!'}, + timeout=10, verify=False) +token = login_resp.json()['token'] +headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} + +# 2. Get claimed products (for source_url) AND listings (for title/price) +products = requests.get('https://www.atomlisting.com/api/v1/products/claimed?page=1&limit=100', + headers=headers, timeout=30, verify=False).json() +listings = requests.get('https://www.atomlisting.com/api/v1/listings', + headers=headers, timeout=30, verify=False).json() + +# 3. Match listing → product via claimed_product_id to get 1688 source_url +matched = [] +for listing in listings: + if not listing.get('html_url'): # skip incomplete + continue + product = next((p for p in products if p['id'] == listing['claimed_product_id']), None) + if product and product.get('source_url'): + matched.append({'listing': listing, 'product': product}) + +# 4. Build Miaoshou import xls using OFFICIAL template format +workbook = xlwt.Workbook() +sheet = workbook.add_sheet('Worksheet') + +# Official columns (verified by downloading template from Miaoshou UI): +sheet.write(0, 0, '链接地址(必填)') +sheet.write(0, 1, '产品标题') +sheet.write(0, 2, '价格(RMB)') +sheet.write(0, 3, '促销价(RMB)') +sheet.write(0, 4, '提示:促销价仅可用于采集到Lazada') + +for idx, item in enumerate(matched, start=1): + listing = item['listing'] + product = item['product'] + pf = listing.get('platform_fields', {}) + sheet.write(idx, 0, product['source_url']) # ✅ 1688 link, NOT html_url + sheet.write(idx, 1, listing['title']) + sheet.write(idx, 2, pf.get('price_cny', '')) + sheet.write(idx, 3, '') + sheet.write(idx, 4, '') + +workbook.save('/tmp/miaoshou_import.xls') +``` + +**5. Upload to Miaoshou via browser automation:** +```python +# Prerequisites: logged into Miaoshou via DrissionPage (see captcha-auto-login skill) +page.get('https://erp.91miaoshou.com/common_collect_box/index?fetchType=importCopy') +time.sleep(8) + +# Step 1: Close notification popups (critical — they block interactions) +for _ in range(5): + page.run_js(''' + var btns = document.querySelectorAll("button, .jx-dialog__close, .jx-dialog__headerbtn"); + for (var i = 0; i < btns.length; i++) { + var text = btns[i].textContent.trim(); + var ariaLabel = btns[i].getAttribute('aria-label') || ''; + var cls = btns[i].className || ''; + if (text === "关闭" || text === "我知道了" || ariaLabel.includes("关闭") + || cls.includes('close') || cls.includes('headerbtn')) { + btns[i].click(); + } + } + ''') + time.sleep(2) + +# Step 2: Open import dialog +page.run_js(''' + var btns = document.querySelectorAll("button"); + for (var i = 0; i < btns.length; i++) { + if (btns[i].textContent.trim().includes("导入链接采集")) { + var evt = new MouseEvent("click", { bubbles: true, cancelable: true, view: window }); + btns[i].dispatchEvent(evt); + return true; + } + } + return false; +''') +time.sleep(8) + +# Step 3: Upload file via hidden input +file_input = page.ele('css:.jx-upload__input', timeout=5) +file_input.input('/tmp/miaoshou_import.xls') +time.sleep(5) + +# Step 4: Click confirm +page.run_js(''' + var modals = document.querySelectorAll(".jx-dialog"); + for (var i = 0; i < modals.length; i++) { + var style = window.getComputedStyle(modals[i]); + if (style.display !== "none") { + var btns = modals[i].querySelectorAll("button"); + for (var j = 0; j < btns.length; j++) { + if (btns[j].textContent.trim() === "确认") { + btns[j].click(); + return true; + } + } + } + } + return false; +''') + +# Step 5: Wait for parsing (1-5 minutes) +time.sleep(180) +# Status will show: 等待解析 → 解析成功 (X/Y/Z) or 解析失败 +``` + +**Expected result**: `解析成功 3/0/3` (all succeeded) — NOT `解析失败`. + +**If it shows "等待解析"**: Wait 1-5 minutes then refresh. If it shows "解析失败 解析文件数据错误", the template format is wrong. If it shows "导入文件未识别到:【产品主编号】表头", you're using the wrong import method or an outdated template. + +### Alternative: Full Data Injection (Bypass Scraping) +If link scraping fails, use the **complete data** from `platform_fields` to directly create products in Miaoshou or target platforms: +- Title, description, price, quantity, SKU, UPC, GTIN, EAN +- Package dimensions & weight +- Image URLs (semicolon-separated COS links) +- Keywords + +This bypasses Miaoshou's collection box entirely and goes straight to platform-specific CSV generation. + +## Pitfalls + +- **✅ CRITICAL: `html_url` does NOT work with Miaoshou** — Miaoshou's scraper only supports 1688/Taobao/Pinduoduo. You MUST use `source_url` from `/api/v1/products/claimed` (original 1688 link). Using `html_url` produces `解析失败 0/3/3` 100% of the time. +- **Wrong template = "产品主编号" error** — If you see "导入文件未识别到:【产品主编号】表头", you are either using the wrong import method (e.g., "Excel表格导入" instead of "导入链接采集") or an outdated template. The correct "导入链接采集" template has only: `链接地址(必填), 产品标题, 价格(RMB), 促销价(RMB)`. +- **Notification popups block UI** — Miaoshou shows "我知道了" notification popups that MUST be closed before clicking "导入链接采集". Failure to close them causes the click to do nothing. +- **Sub-account permission limits** — The account `xiaochaoren2026` is a Miaoshou sub-account. It can access `采集箱/导入采集` but shows "暂无当前模块权限" on product publish/edit pages. Use a main account for full publishing. +- **店小秖 (Dianxiaomi) requires slider CAPTCHA** — Login page has "拖动下方拼图完成验证" which is extremely difficult to bypass automatically. Prefer Miaoshou for automation. +- **API is partially documented** — 2025-05 upgrade added many endpoints. Some new endpoints may have undocumented body fields or response shapes. Monitor 404/500 responses. +- **Token expiry** — JWT has `exp` field. Use `POST /api/v1/auth/refresh` with refresh token if needed. +- **Image format mix** — AtomK stores both `.jpeg` and `.webp` images. Some platforms prefer `.jpeg` only. Use `POST /api/v1/products/images/from-url` or `from-file` for image management. +- **COS URLs are public** — but tied to AtomK's Tencent Cloud account. If AtomK changes storage policy, URLs may break. +- **No batch export API** — `/api/v1/export` does not exist. Export is done product-by-product via pre-generated `csv_url`/`html_url`. +- **ERP accounts should NOT be stored in Stores** — use `/api/v1/settings/erp-accounts` instead. The old workaround of `platform=woocommerce` + `extra_data.real_platform=miaoshou_erp` is deprecated. +- **`POST /settings/erp-accounts` has 500 bug (2025-05)** — all creation attempts fail regardless of platform. Store ERP credentials in local file (`~/.hermes/credentials/accounts.md`) until backend is fixed. +- **`password` in `extra_data` auto-filtered to `***`** — always use `pwd` field for any account type (stores, ERP, email). +- **Silent UI Failures for Account Linking** — `POST /api/v1/stores` may return 502 (telnumber DB constraint), `POST /api/v1/settings/social-accounts` may return 500 for unsupported platforms. Intercept requests to debug. +- **Social platform whitelist** — only `facebook`, `tiktok`, `twitter`, `wordpress` work. `instagram`, `pinterest` return 500. +- **Sync API needs API key** — `GET /settings/sync/status` and `POST /settings/sync/center-to-local` require `x-api-key` header, not JWT. +- **`status="imported"` triggers 500** — always use `status="generated"` when creating listings. +- **`platform_fields` is silently ignored in POST** — you must use PUT to set it after creation. +- **`claimed_product_id` must be unique** — assigning the same product ID to multiple listings creates duplicates. +- **API returns max 100 listings per page** — use `page` parameter to paginate. +- **502 Bad Gateway on rapid fire** — add 50ms delay between requests, or implement retry with backoff. +- **Consecutive 500s mean a bad product** — if you get 10 consecutive 500s, skip that product. The product data itself is likely invalid. +- **Deduplicate before creating** — always check existing listings first. Re-running a sync script without dedup creates duplicates. +- **`POST /api/v1/settings/sync/center-to-local`** requires admin API key, not regular JWT. +- **Ozon Seller API keys expire** — both Hzqjone(client_id=3098640) and Hzqjtwo(client_id=3103005) keys returned "Invalid Api-Key" as of 2025-05. Must regenerate from seller.ozon.ru (requires Russian IP). Ozon product import uses `/v3/product/import` (not `/v1/`). +- **Ozon publish not supported** — `POST /listings/:id/publish` returns 400 for Ozon. Must use Ozon Seller API directly to create products. + +### Stores API Details + +**Working platform values**: `walmart`, `temu`, `ozon`, `aliexpress`, `amazon`, `ebay`, `woocommerce`, `lazada`, `shopee` + +**Store fields**: +| Field | Description | +|-------|-------------| +| `id` | Store ID | +| `platform` | E-commerce platform (strict validation) | +| `store_name` | Human-readable name | +| `store_url` | Platform URL | +| `extra_data` | Arbitrary JSON (API keys, credentials, etc.) | +| `is_active` | Boolean | + +**Pitfalls for Stores API**: +- **`password` in `extra_data` auto-filtered to `***`** — use `pwd` field instead. +- **Platform validation is strict** — `miaoshou`, `shopify`, `other`, `erp` all return 500. Only e-commerce platforms accepted. +- **ERP accounts do NOT belong in Stores** — use `/api/v1/settings/erp-accounts` instead. +- **`POST /stores/:id/test-connection`** — validates store credentials (API key, URL, etc.) +- **`GET /stores/:id/categories`** — fetches platform category tree (not all platforms support this; walmart returns 400) + +### Settings API Details + +**`GET /api/v1/settings/accounts`** — 聚合所有账号信息(stores + social + erp + email),一站式获取。 + +**`GET /api/v1/settings/system-code`** — 公开端点,无需认证。返回 `{"system_code": "AK-101"}`。 + +**ERP Accounts CRUD** (`/api/v1/settings/erp-accounts`): +- **必填字段**: `erp_name`, `account_name` +- **可选字段**: `platform`, `extra_data`, `is_active` +- ⚠️ **POST currently returns 500 (backend bug, 2025-05)** — all platform values fail. Workaround: store ERP credentials in `/home/ubuntu/.hermes/credentials/accounts.md` until backend is fixed. +- When fixed, expected usage: +```python +erp = { + "erp_name": "妙手ERP", + "platform": "miaoshou", + "account_name": "17762501033", + "extra_data": {"phone": "17762501033", "pwd": "Tt123456!"}, + "is_active": True +} +requests.post(f'{BASE}/settings/erp-accounts', json=erp, headers=headers) +``` + +**Email Accounts CRUD** (`/api/v1/settings/email-accounts`): +- **必填字段**: `email_provider`, `email_address`, `account_name` +- **可选字段**: `smtp_host`, `smtp_port`, `smtp_username`, `use_tls`, `extra_data`, `is_active`, `linked_store_id` +- 密码存 `extra_data.pwd`(不要用 `password`) +```python +email = { + "email_provider": "gmail", + "email_address": "admin@9webs.cn", + "account_name": "9webs Admin", + "smtp_host": "smtp.gmail.com", + "smtp_port": 587, + "smtp_username": "admin@9webs.cn", + "use_tls": True, + "extra_data": {"pwd": "xxx"}, + "is_active": True +} +``` + +**Social Accounts CRUD** (`/api/v1/settings/social-accounts`): +- **字段**: `platform`, `account_name`, `account_handle`, `linked_store_id`, `is_active` +- **可用platform**: `facebook`, `tiktok`, `twitter`, `wordpress` +- ⚠️ `instagram`, `pinterest` 返回 500 (backend bug) + +### Social Posts API + +Social模块管理社交帖子(Facebook、Twitter等),结构类似Listings: +- **`GET /social/posts`** — 帖子列表 +- **`POST /social/posts/generate`** — AI生成社交帖子内容 +- **`POST /social/posts/:id/publish`** — 发布到社交平台 +- **`GET /social/listings`** — 获取与社交帖子关联的listing数据 + +**Social Post fields**: +| Field | Description | +|-------|-------------| +| `id` | Post ID | +| `platform` | Social platform | +| `content` | Post content text | +| `images` | Image URLs | +| `status` | `draft` / `generated` / `published` | +| `linked_listing_id` | Optional link to a listing | + +### Sync API Details + +**`POST /api/v1/settings/sync/center-to-local`**: +- 从中心数据库同步到本地实例 +- ⚠️ 需要 admin API key(`x-api-key` header),非JWT token。普通JWT返回401。 +- 用于多实例部署场景 + +**`GET /api/v1/settings/sync/status`**: +- 查询当前同步状态 +- 同样需要 API key + +### Listings Generate & Publish + +**`POST /api/v1/listings/generate`** — AI生成listing数据: +- 请求体: `{"claimed_product_id": 123, "platform": "ozon", "store_id": 12}` +- 自动生成: 目标语言标题/描述/关键词(Ozon=俄语)、EAN/UPC/GTIN条码、定价(汇率6.8换算)、包裹尺寸、分类路径 +- 耗时约30-60秒/个,需设timeout=120+ +- 返回完整listing对象(含id/title/description/platform_fields/csv_url/html_url等) +- 批量生成时需逐个调用,不能并行。每个generate是同步的(不是async) + +**`POST /api/v1/listings/:id/publish`** — 推送到目标平台: +- 前提: store已配置且 `test-connection` 通过 +- ⚠️ **Ozon不支持** — 返回400 "Platform ozon publishing not supported yet" +- Walmart/Temu/AliExpress可能支持(未测试) +- Ozon上架需走Ozon Seller API (`/v3/product/import`) 直接推 + +**批量Generate模式** (推荐): +```python +# 逐个generate,带timeout和重试 +import requests, time + +s = requests.Session() +s.verify = False +# ... login ... + +# 获取未使用的claimed products +products = [] # 从 /products/claimed 获取 +used = set() # 从 /listings 获取已用的claimed_product_id +unused = [p for p in products if p['id'] not in used] + +for i, prod in enumerate(unused[:N]): + try: + r = s.post(f'{BASE}/listings/generate', json={ + 'claimed_product_id': prod['id'], + 'platform': 'ozon', + 'store_id': 12, + }, timeout=120) + if r.status_code == 200: + listing = r.json() + print(f"[{i+1}] OK id={listing['id']} title={listing.get('title','')[:40]}") + else: + print(f"[{i+1}] FAIL {r.status_code}: {r.text[:100]}") + except requests.exceptions.ReadTimeout: + print(f"[{i+1}] TIMEOUT (product {prod['id']} may be too complex)") + time.sleep(0.5) +``` + +## Verification Steps +1. Confirm `api/v1/listings` returns expected data shape and `claimed_product_id` matches `api/v1/products/claimed` items. +2. Confirm `source_url` exists on claimed products and points to `detail.1688.com`. +3. Generate a 1-row test XLS with official template format (`链接地址(必填), 产品标题, 价格(RMB)`) using `source_url`. +4. Upload to Miaoshou via "导入链接采集". Wait 1-5 minutes. Expected: `解析成功 1/0/1`. +5. If you see `解析失败`, check: (a) URL is 1688 not AtomK HTML, (b) template downloaded from Miaoshou UI is current, (c) popup notifications were closed before upload. + +## Listings API — Write Side (Create / Update / Delete) + +### Create Listing +``` +POST https://www.atomlisting.com/api/v1/listings +``` +**Required body fields**: +```python +{ + "claimed_product_id": 123, # must be a valid, unassigned claimed product ID + "platform": "ozon", # platform name + "store_id": 12, # store ID from /api/v1/stores + "title": "Product Title", + "description": "Description", + "price": 9.99, + "currency": "CNY", + "status": "generated" # NOT "imported" — that causes 500! +} +``` + +**Pitfalls for Create**: +- **`status="imported"` triggers 500** — always use `status="generated"`. +- **`platform_fields` is silently ignored in POST** — you must use PUT to set it after creation. +- **`claimed_product_id` must be unique** — assigning the same product ID to multiple listings creates duplicates. Track which product IDs are already used. +- **API pagination limit is 100** — `GET /listings?limit=100&page=N`. `limit>100` returns empty items. + +### Update Listing (Set platform_fields) +``` +PUT https://www.atomlisting.com/api/v1/listings/{id} +``` +```python +{ + "platform_fields": { + "product_id": "123456789", + "offer_id": "SKU-001", + "store_name": "Hzqjone", + "url": "https://www.ozon.ru/product/123456789" + } +} +``` + +### Delete Listing +``` +DELETE https://www.atomlisting.com/api/v1/listings/{id} +``` + +### Bulk Sync Pattern (Ozon → AtomK) + +When syncing external platform listings (Ozon, Temu, etc.) into AtomK, use this pattern: + +```python +import requests, json, time +from urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +BASE = 'https://www.atomlisting.com/api/v1' +s = requests.Session() +s.verify = False + +# 1. Login +r = s.post(f'{BASE}/auth/login', json={'username': 'admincao', 'password': 'Tt123456!'}, timeout=10) +s.headers.update({'Authorization': f'Bearer {r.json()["token"]}'}) + +# 2. Fetch ALL existing listings with pagination (max 100 per page) +all_listings = [] +page = 1 +while True: + r = s.get(f'{BASE}/listings', params={'page': page, 'limit': 100}, timeout=15) + d = r.json() + items = d if isinstance(d, list) else d.get('items', []) + all_listings.extend(items) + if len(items) < 100: + break + page += 1 + +# 3. Build index: product_id -> [listing_ids] for dedup +ozon_listings = [l for l in all_listings if l.get('platform') == 'ozon'] +by_pid = {} +for l in ozon_listings: + pf = l.get('platform_fields') or {} + pid = pf.get('product_id') + if pid: + by_pid.setdefault(pid, []).append(l) + +# 4. Delete duplicates (keep first, delete rest) +for pid, lst in by_pid.items(): + for l in lst[1:]: + s.delete(f'{BASE}/listings/{l["id"]}', timeout=10) + time.sleep(0.05) + +# 5. Get unused claimed_product_ids +products = [] +page = 1 +while True: + r = s.get(f'{BASE}/products/claimed', params={'page': page, 'limit': 100}, timeout=15) + items = r.json() if isinstance(r.json(), list) else r.json().get('items', []) + products.extend(items) + if len(items) < 100: break + page += 1 + +used_pids = {l['claimed_product_id'] for l in ozon_listings} +unused = [p['id'] for p in products if p['id'] not in used_pids] + +# 6. Create missing listings +existing_pids = set(by_pid.keys()) +for i, product_data in enumerate(external_products): + if product_data['product_id'] in existing_pids: + continue + if not unused: + print("No more unused claimed_product_ids!") + break + + cp_id = unused.pop(0) + # POST to create (without platform_fields) + r = s.post(f'{BASE}/listings', json={ + 'claimed_product_id': cp_id, + 'platform': 'ozon', + 'store_id': store_id, + 'title': product_data.get('name', f'Ozon-{product_data["product_id"]}'), + 'description': '', + 'price': product_data.get('price', 0), + 'currency': 'CNY', + 'status': 'generated', + }, timeout=10) + + if r.status_code == 201: + listing_id = r.json()['id'] + # PUT to set platform_fields + s.put(f'{BASE}/listings/{listing_id}', json={ + 'platform_fields': { + 'product_id': str(product_data['product_id']), + 'offer_id': product_data.get('offer_id', ''), + 'store_name': store_name, + 'url': f'https://www.ozon.ru/product/{product_data["product_id"]}', + } + }, timeout=10) + time.sleep(0.05) # rate limit protection +``` + +**Critical Pitfalls for Bulk Sync**: +- **Pagination is mandatory** — `/listings` returns max 100 items. Always paginate or you'll create hundreds of duplicates. +- **Two-step creation** — POST creates the listing, but `platform_fields` must be set via a separate PUT. The POST silently ignores `platform_fields`. +- **`status="imported"` = 500** — use `"generated"`. +- **502 Bad Gateway on rapid fire** — add 50ms delay between requests, or implement retry with backoff. +- **Consecutive 500s mean a bad product, not rate limiting** — if you get 10 consecutive 500s, skip that product. The product data itself is likely invalid (e.g., null fields the backend can't handle). +- **Deduplicate before creating** — always check existing listings first. Re-running a sync script without dedup creates 2x, 3x duplicates. + +## References + +- `references/backend-development.md` — Full guide for adding new API endpoints: model creation, router setup, Alembic migration, production deployment (no CI/CD, manual SSH), plus a debug pattern for Desktop-backend API mismatch errors. +- `references/billionmail-api.md` — BillionMail (SG4) email server API reference: mailbox creation, SMTP relay, and default domain (`9webs.site`). + + +## Related Skills +- `atomk-temu-batch-listing` — browser-based batch claiming and AI generation +- `miaoshou-collect-box` — Miaoshou ERP collection box navigation +- `captcha-auto-login` — automated ERP login with CAPTCHA solving \ No newline at end of file