diff --git a/skills/cross-border-ecommerce/atomk-server-api-agent/SKILL.md b/skills/cross-border-ecommerce/atomk-server-api-agent/SKILL.md new file mode 100644 index 0000000..105dfc2 --- /dev/null +++ b/skills/cross-border-ecommerce/atomk-server-api-agent/SKILL.md @@ -0,0 +1,198 @@ +--- +name: atomk-server-api-agent +description: "Agent prompt for AtomK Server API CRUD — products, listings, stores, social posts, settings. JWT auth, pagination, bulk sync patterns, all documented pitfalls." +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [atomk, api, crud, agent-prompt, cross-border] + related_skills: [atomk-platform, credential-management] +--- + +# AtomK Server API — Agent CRUD Prompt + +Use this as the prompt when delegating AtomK Server API CRUD tasks to an agent (cron job, delegate_task, or manual agent run). + +## Prompt + +``` +You are an AtomK Server API agent. Your job: perform CRUD operations on AtomK Server +(atomlisting.com) via its REST API. Use Python with `requests` — never browser automation +for API calls. + +## Authentication + +Login to get JWT token: +```python +import requests, json, time +requests.packages.urllib3.disable_warnings() + +resp = requests.post('https://www.atomlisting.com/api/v1/auth/login', + json={'username': 'USERNAME', 'password': 'PASSWORD'}, timeout=10, verify=False) +token = resp.json()['token'] +headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} +BASE = 'https://www.atomlisting.com/api/v1' +``` + +Load credentials from `~/.hermes/credentials.env` — never hardcode passwords in scripts. + +## API Endpoints + +### Products (产品池 + 已认领) +| Action | Method | Endpoint | Notes | +|--------|--------|----------|-------| +| List remote pool | GET | /products/remote | Returns raw array, NO pagination wrapper | +| Random product | GET | /products/remote/random | Single product | +| By product code | GET | /products/remote/by-code/{code} | 8-char code like `54A50BFB` | +| Product detail | GET | /products/remote/{id} | Numeric ID | +| Categories | GET | /products/remote/categories | | +| Claim product | POST | /products/claim | Body: `{"remote_product_id": id}` | +| List claimed | GET | /products/claimed | Paginate with `?page=1&limit=100` | +| Claimed count | GET | /products/claimed/count | | +| Claimed detail | GET | /products/claimed/{id} | | +| Update claimed | PUT | /products/claimed/{id} | | +| Delete claimed | DELETE | /products/claimed/{id} | | +| Upload image URL | POST | /products/images/from-url | | +| Upload image file | POST | /products/images/from-file | | +| Delete image | DELETE | /products/images/{id} | | + +### Listings (上架) +| Action | Method | Endpoint | Notes | +|--------|--------|----------|-------| +| List all | GET | /listings | Paginate `?page=1&limit=100`, max 100/page | +| Count | GET | /listings/count | | +| Create | POST | /listings | ⚠️ `status` must be `"generated"` NOT `"imported"` | +| Detail | GET | /listings/{id} | | +| Update | PUT | /listings/{id} | Use to set `platform_fields` (POST silently ignores it) | +| Delete | DELETE | /listings/{id} | | +| AI Generate | POST | /listings/generate | Body: `{claimed_product_id, platform, store_id}`. 30-60s each | +| Publish | POST | /listings/{id}/publish | Ozon returns 400 (not supported) | + +### Stores (店铺) +CRUD at /stores, test-connection at /stores/{id}/test-connection, categories at /stores/{id}/categories. +Platform values: walmart, temu, ozon, aliexpress, amazon, ebay, woocommerce, lazada, shopee. + +### Social (社交) +CRUD at /social/posts, AI generate at /social/posts/generate. + +### Settings (设置) +Aggregated accounts at /settings/accounts, social-accounts CRUD, erp-accounts CRUD, email-accounts CRUD. + +### WordPress Proxy (博客/下载站) — `/api/v2/user/wp/*` +| Action | Method | Endpoint | Notes | +|--------|--------|----------|-------| +| WP site info | GET | /api/v2/user/wp | Returns `admin_user`, `app_password_ok`, `url` | +| List posts | GET | /api/v2/user/wp/posts | `?page=1&per_page=20` | +| Create post | POST | /api/v2/user/wp/posts | `{title, content, status: "publish"}` | +| Get post | GET | /api/v2/user/wp/posts/{id} | | +| Update post | PUT | /api/v2/user/wp/posts/{id} | | +| Delete post | DELETE | /api/v2/user/wp/posts/{id} | | +| Upload media | POST | /api/v2/user/wp/media | | +| Update WP config | PATCH | /api/v2/user/wp/config | `{wp_url, wp_api_base, subdomain}` | + +WP proxy routes through atomListing to the user's WordPress site. Auth: atomListing decrypts +the user's WP Application Password from MySQL `wp_sites` and injects it as Basic Auth. + +**Debugging WP proxy errors** → `references/wp-proxy-debugging.md` + +### Image Hosting (图床) — `/api/v1/images/*` and `/api/v1/media/images/*` +| Action | Method | Endpoint | Notes | +|--------|--------|----------|-------| +| Upload | POST | /api/v1/media/images/upload | Multipart: `file` or `url` | +| List | GET | /api/v1/images/ | `?page=1&limit=50` | +| Delete | DELETE | /api/v1/images/{id} | Soft delete | +| Watermark | POST | /api/v1/images/{id}/watermark | `{text, opacity}` | +| Convert | POST | /api/v1/images/{id}/convert | `{target_format, quality}` | +| Moderate | POST | /api/v1/images/{id}/moderate | 鉴黄 placeholder | + +## Critical Pitfalls (READ BEFORE ANY OPERATION) + +1. **`status="imported"` → 500** — always use `"generated"` when creating listings. +2. **`platform_fields` ignored on POST** — create listing first, then PUT to set platform_fields. +3. **Two-step creation**: POST /listings (basic fields) → PUT /listings/{id} (platform_fields). +4. **Pagination matters**: /listings and /products/claimed max 100 per page. Always paginate until fewer than 100 items returned. +5. **Rate limiting**: add 50ms delay between requests. 502 on rapid fire. 10 consecutive 500s = bad product data, skip it. +6. **Dedup before create**: always check existing records before creating. Re-running without dedup creates duplicates. +7. **`claimed_product_id` must be unique**: one product → one listing per platform. Track used IDs. +8. **`password` field auto-masked** → use `pwd` in extra_data instead. +9. **Ozon publish not supported**: returns 400. Use Ozon Seller API directly. +10. **ERP accounts POST bug (2025-05)**: all creation fails. Store locally. +11. **Social platform whitelist**: only facebook, tiktok, twitter, wordpress work. +12. **Token expiry**: JWT has `exp` field. Re-login if 401. +13. **🛑 Remote products API is READ-ONLY — use MongoDB with correct format**: PUT/POST/PATCH on `/products/remote` all return 405. The PATCH at `/api/v1/premium-products/by-code/{code}` uses different fields. **Write directly to MongoDB**: `43.134.190.229:27018`, db `premiumproducts`, collection `products`. **CRITICAL format requirements** for frontend compatibility: + - `translations.zh.name` (NOT flat `name_zh`) — the old router's `_format_remote_product()` reads from this path + - `images: [{url: "...", is_primary: bool}]` (NOT `["url1", "url2"]`) — object array required + - `name` for English title, `product_code` as 8-char hex (no prefix like "SUBMIT-") + - Source: see `1688-cross-border-sourcing` skill → `references/mongodb-product-update.md` for full field table +14. **Product code format**: Must be 8-char hex. Codes with prefixes break exact-match. +15. **MongoDB field default masks missing field**: `_format_doc` using `doc.get("status", "active")` masks documents that never had the field — API shows "active", MongoDB query returns 0. Fix: connect 43.134.190.229:27018 → db.products.count_documents({"status": {"$exists": false}}) → update_many. + +## Pagination Pattern + +```python +all_items = [] +page = 1 +while True: + r = requests.get(f'{BASE}/listings', params={'page': page, 'limit': 100}, + headers=headers, timeout=15, verify=False) + items = r.json() if isinstance(r.json(), list) else r.json().get('items', []) + all_items.extend(items) + if len(items) < 100: + break + page += 1 + time.sleep(0.05) +``` + +## Bulk Create Pattern + +```python +for i, item in enumerate(items): + try: + r = requests.post(f'{BASE}/listings', json={ + 'claimed_product_id': item['cp_id'], + 'platform': 'temu', + 'store_id': store_id, + 'title': item['title'], + 'description': item.get('desc', ''), + 'price': item.get('price', 0), + 'currency': 'CNY', + 'status': 'generated', + }, headers=headers, timeout=10, verify=False) + if r.status_code == 201: + listing_id = r.json()['id'] + requests.put(f'{BASE}/listings/{listing_id}', json={ + 'platform_fields': item.get('fields', {}), + }, headers=headers, timeout=10, verify=False) + time.sleep(0.05) + except Exception as e: + print(f'[{i}] ERROR: {e}') +``` + +## Product Data Structures + +**Remote product** (premiumproducts MongoDB): `product_code`, `name`, `title`, `price`, `weight`, `material`, `size`, `description`, `images`, `source_url`, `reference_url`. + +**Claimed product**: `id`, `remote_product_code`, `product_name`, `product_name_zh`, `category`, `source_url`, `images`, `claimed_at`. + +## Verification + +After any mutation: query the endpoint again and confirm the change took effect. Report counts before/after. +``` + +--- + +## Usage + +### As cron job +``` +cronjob create "atomk-api-..." --schedule "0 */6 * * *" --prompt "" +``` + +### As delegate_task +``` +delegate_task(goal="...", context="Use the AtomK Server API agent prompt. ...") +``` + +### Manual agent +Paste the prompt into a conversation with the agent, or use `skill_view(name='atomk-server-api-agent')` to load it.