Skip to main content

noon-shared

Common noon Seller Center mechanics — login (OTP auto-fetch), page-structure URL map, My Catalog read access, common modals, button-click patterns, project-ID discovery. Prerequisite: every other noon-* skill (noon-listing, noon-fbn, noon-exports, noon-ads) expects this loaded for auth and shared UI patterns.

跳到安装

来源信息

仓库
zpoint/vibe-seller
最近来源活动
2026年8月12日 03:15
检测到的 SKILL.md 语言
英语
星标
68
分支
14

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
2 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
noon-shared
description
Common noon Seller Center mechanics — login (OTP auto-fetch), page-structure URL map, My Catalog read access, common modals, button-click patterns, project-ID discovery. Prerequisite: every other noon-* skill (noon-listing, noon-fbn, noon-exports, noon-ads) expects this loaded for auth and shared UI patterns.
# Noon — Shared (login, navigation, common patterns) This skill covers what every noon Seller Center task needs: authentication, the page-structure URL map, My Catalog read access, and shared UI patterns (modals, button clicks). Operation-specific skills (`noon-listing`, `noon-fbn`, `noon-exports`, `noon-ads`) load this first. ## 1. Login — Auto-fetch OTP When Email Is Bound Noon uses **email OTP only** (no password). Before doing ANYTHING, decide how the OTP will be retrieved — do NOT bother the user if an email MCP integration can do it for you. ### Decision flow (follow in order, do NOT skip steps) 1. Open the login page and look at the email it's about to send OTP to: ```bash browser-use <<'PY' new_tab("https://login.noon.partners/en/") wait_for_load() print(page_info()) # find the email on the "We've sent..." screen # OR the prefilled channelIdentifier PY ``` Ziniao often auto-fills or remembers a session. The page shows `Log in with OTP sent to <email>`. 2. Check whether the task prompt includes a **"## Email System"** section (the runner injects it when the store has connected email accounts). Compare the bound emails to the Noon login email shown on screen: - **Match** → fetch the OTP yourself via email MCP (Case A). - **No email system section**, OR **emails do not match** → ask the user (Case B). Never guess — mismatched emails mean a different inbox. ### Case A — Auto-fetch OTP via email MCP (preferred) This works when the bound email address equals the Noon login email. One round trip, no user prompt: ```python # 1. Trigger immediate IMAP poll (don't wait 5 min for auto-sync): vibe_seller_sync_email_now(account_email='<bound-email>') # 2. Get the per-account DB path — COPY the exact db_path string it # returns; do NOT hand-build the path from the account id: vibe_seller_email_info(store_id='<store-id>') # Returns: { accounts: [{ email, db_path, ... }] } ``` > ⚠️ **Use the `db_path` string verbatim from `email_info` — never > retype the account UUID into a path.** `sqlite3 <bad-path>` does NOT > error on a missing file: it **creates a new empty DB and returns zero > rows**, so a one-character typo in the UUID silently looks like "no OTP > email arrived." (Live failure: an agent hand-retyped the account UUID > with two hex digits transposed in the tail — `…b7a` where the real id > ended `…ba7` — queried the empty DB it just created, found nothing, > then grepped random 6-digit numbers out of an unrelated dump and > submitted two wrong codes — see the "Invalid OTP" note below.) Then read the **latest** Noon OTP email — the one that arrived *after* you clicked to send the code (noon issues a fresh code on every send / "Try again", and older codes are already invalid). The body is HTML — the 6-digit code sits on its own line inside a styled `<div>`. Extract it from `body_html` (not `body_text`), newest first by `received_epoch`: ```bash sqlite3 "<db_path-from-email_info>" "SELECT body_html FROM emails \ WHERE sender LIKE '%verify@noon.com%' \ AND subject='Verify your email' \ ORDER BY received_epoch DESC LIMIT 1" \ | python3 -c " import sys, re html = sys.stdin.read() # The OTP is a standalone 6-digit number inside a styled div. # Reject repeated digits (000000, 333333 etc. — those are hex colors). for m in re.findall(r'>\s*(\d{6})\s*<', html): if len(set(m)) > 1: print(m); break " ``` If this prints nothing, the DB path is wrong or the sync hasn't landed — re-run `email_info` (copy the path), re-run `sync_email_now`, and retry. **Do NOT** fall back to grepping 6-digit numbers out of other files/dumps; those are IDs/timestamps, not the OTP. Then fill it and continue: ```bash browser-use <<'PY' fill_input("input[name='otp']", "<code>") # OTP field (adjust selector to the live input) js("document.querySelector('button[type=submit]').click()") # Continue wait_for_load() print(page_info()) # dismiss passkey prompt (button has no stable id; match by text): js("Array.from(document.querySelectorAll('button')).find(b=>/maybe later/i.test(b.textContent))?.click()") PY ``` > **The programmatic fill WORKS — noon's OTP input is not anti-bot > protected.** Setting the input via the native > `HTMLInputElement.value` setter + dispatching `input`/`change` (what > `fill_input` does) registers fine: Continue enables and the form > submits. If you enter a **correct, fresh** code it logs you in. Do NOT > assume the input is blocked. > **`Invalid OTP` (with a `Trace ID`) means the CODE is wrong or stale — > NOT that the input was blocked.** The server accepted and validated > your submission; the value you typed just wasn't the current code. This > is the #1 noon-login failure and it is almost always one of: (a) you > read an **empty DB** because the `db_path` was hand-typed with a typo > (see the ⚠️ above — always copy it from `email_info`); (b) you used a > **stale** code from a previous send instead of the newest email; or (c) > you grepped a **random 6-digit number** out of a non-email file. Fix > the retrieval and re-fill — do NOT conclude "anti-bot" and give up. > (Live failure this note fixes: an agent queried an empty typo'd DB, > submitted `127660` then `667599` — neither was a real code — got > `Invalid OTP` twice, and wrongly reported noon as "anti-bot blocked".) > **Only escalate to Case B if a verified-fresh, correct code is rejected > repeatedly** (re-fetched from the right DB, newest email, entered > exactly) **or** the account genuinely can't receive mail. A scheduled > unattended run has no human to type the OTP, so a real > can't-retrieve-the-code situation must surface as an explicit > "noon needs re-login" escalation, not a silent partial report. Once a > login succeeds, the Ziniao session cookie persists across later tasks. ### Case B — Ask the user (only when Case A doesn't apply) Trigger the OTP, then ask via `AskUserQuestion` (which requires **2+ options**, not 1 — a single-option call fails validation): ``` AskUserQuestion(questions=[{ "question": "I've triggered the Noon OTP email to <email>. Please enter the 6-digit code in the browser, then tell me when you're ready.", "header": "OTP", "options": [ {"label": "Entered, continue", "description": "I typed the OTP"}, {"label": "Didn't arrive", "description": "Resend or switch account"} ], "multiSelect": false }]) ``` If the user can't receive the OTP, don't guess or retry — escalate and let them resolve. ### After login — discover `{project_id}` from the URL ```bash browser-use <<'PY' # dismiss passkey prompt (match by text — no stable id) js("Array.from(document.querySelectorAll('button')).find(b=>/maybe later/i.test(b.textContent))?.click()") wait_for_load() print(page_info()) # URL now carries ?project=PRJ{project_id} PY # Redirects to welcome.noon.partners/... with project=PRJ{project_id} ``` **Do NOT ask the user for the project ID** if the store profile (`stores/<slug>/STORE.md`) doesn't have it. Read it from the post- login URL — the welcome / store-home page always carries `?project=PRJ{project_id}` (e.g. `NNNNNN`). Capture it once, then reuse for the rest of the task. A project's countries share the same numeric project ID; only the URL country suffix (`/en-<cc>/`) differs. Optionally persist what you learned back to the store profile: ``` vibe_seller_write_workspace_file( path="stores/<slug>/metadata.json", content='{"platform_countries": {"noon": ["EG", "KW"]}, "noon_project_id": "<project_id>"}' ) ``` ## 2. Page Structure Cheat Sheet Most post-login URLs require `?project=PRJ{project_id}` (the login and welcome pages are the only exceptions). `{project_id}` is the numeric project identifier. The store identifier in path segments like `STR{project_id}-N{CC}` reuses the same numeric value with `STR` prefix and country suffix. Direct URL navigation works for most pages; sidebar only for Support/Help. > ⚠️ **Two different country mechanisms — do not assume one from the > other.** Portals whose path carries `/en-{cc}/` (Ad Manager, FBN) are > country-scoped **by URL**: navigate and you are in that country. > Portals whose path is only `/en/` (**My Catalog, Imports, Exports, > Sales, Transaction View, Vantage**) are scoped by a **sidebar store > switcher** that persists across sessions — the URL is identical for > every country, so a page can silently show a DIFFERENT market than the > one you were just working in. > > **Read the flag before trusting any `/en/` page**, and say which > country the data is from when you report it: > ```bash > browser-use <<'PY' > print(js("""var f=document.querySelector('.ns-side-nav__store-flag'); > return f ? getComputedStyle(f).backgroundImage : 'no switcher';""")) > # → url("…/images/flags/<cc>.svg") > PY > ``` > To switch, click the `.ns-store-list__item` whose `[class*=item-flag]` > background-image ends in the target `<cc>.svg` — the entries can share > an identical store NAME across countries, so match on the flag, never > on the label. > > Live failure this prevents: a whole-catalogue stock/live-status audit > was read off this page while the switcher sat on a neighbouring > market, and the resulting "these SKUs have stock but no ads" list was > used to plan campaigns in the *other* country. Nothing in the URL, > the page title, or the project id revealed the mismatch. | Page | URL | |------|-----| | Create listing | `noon-catalog.noon.partners/en/catalog/create` | | Edit listing | `noon-catalog.noon.partners/en/catalog/{sku}/d?code={code}&offerTab=noon` | | My Catalog | `noon-catalog.noon.partners/en/catalog` | | Catalog Imports | `noon-catalog.noon.partners/en/imports` | | Catalog Exports | `noon-catalog.noon.partners/en/exports` | | FBN My ASN & Storage | `fbn.noon.partners/en-{cc}/asn` | | FBN Create ASN | `fbn.noon.partners/en-{cc}/asn/createasn` | | FBN My Inventory | `fbn.noon.partners/en-{cc}/inventory` | | Sales | `reports.noon.partners/en/sales/` | | Transaction View | `noon-payments.noon.partners/en/transaction-view` | | Ad Manager | `admanager.noon.partners/en-{cc}/home?mpCode=noon` | | Campaign Detail | `admanager.noon.partners/en-{cc}/campaign/details/{id}?mpCode=noon` | | Create Campaign | `admanager.noon.partners/en-{cc}/campaign/start?mpCode=noon` | | Vantage | `vantage.noon.partners/en/` | ## 3. My Catalog (read access) **URL**: `https://noon-catalog.noon.partners/en/catalog?project=PRJ{project_id}` Tabs: `noon` (default), `supermall`, `global`. Each row has: - Product title + Brand - PSKU (Partner SKU) + SKU (noon ID), copyable - Price, Sale/Promo badge - Estimated Fees (FBN + FBP separately) - Active Net Stock (FBN + FBP, links to inventory) - Performance (Views, Units Sold, Sales) - Seller Status toggle - Live Status + "View Issues" link Click the product title anchor to open the edit page (see `noon-listing` skill). ### `wait_for_load()` is NOT enough — poll until the rows render noon's portal is a React/Next app that reaches `readyState:'interactive'` with `document.body.innerText === ''` and paints seconds later. Right after `wait_for_load()` you will read an **empty page**: `js(...)` returns `''`, a row count returns `0`, and a `print()` of it makes the heredoc emit *nothing at all* (`Bash completed with no output`). None of that means "no results" — it means "not painted yet". Concluding `0 items` from it is a silent wrong answer. Always poll for a content marker before reading anything: ```bash browser-use <<'PY' import time new_tab("https://noon-catalog.noon.partners/en/catalog?project=PRJ{pid}&tab=noon") wait_for_load() t = "" for _ in range(15): # ~45 s worst case t = js("return document.body.innerText") or "" if "PSKU" in t: # the marker for THIS page break time.sleep(3) else: raise SystemExit("catalog never rendered — do not treat as empty") print(len(t)) PY ``` Pick the marker per page: `PSKU` on My Catalog, `Barcode` on the Offer tab, `Total`/`items` for a count. Never `time.sleep(n)` once and hope. ### Enumerate a subset via URL params — do NOT fight the search box The list page's filters and pagination are **URL state**. Set them in the URL and read the rows; that is exact, resumable, and avoids the search box entirely: ``` …/en/catalog?project=PRJ{project_id}&tab=noon&live_status=false&page=1 ``` | Param | Values | Notes | |---|---|---| | `tab` | `noon` / `supermall` / `global` | marketplace, not country | | `live_status` | `false` / `true` | `false` = Not Live only | | `page` | 1-based | 20 rows/page; the header reads `N items`, so pages = ⌈N/20⌉ | Read `N items` first, then walk `page=1…⌈N/20⌉`. **Anchor `href`s carry the `code=` param you need** for each row's detail page — collect `(PSKU, href)` while you page, don't reconstruct URLs later (§ below). > **One page per `browser-use` invocation.** The store wrapper kills any > single invocation at **120 s** and counts it as a wedge strike (see > browser-harness § "Budget every invocation"). A render poll can burn > ~45 s on one page, so three pages in one heredoc overruns the limit and > gets read as a broken browser. Loop pages in the **shell**, one > invocation each, appending to a file.
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看