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
آخر نشاط في المصدر
١٢ أغسطس ٢٠٢٦ في ٠٣:١٥
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦٨
التفرعات
١٤

خيارات التثبيت

يُحدَّد 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