重开仓库内容:改推五个技能(browser-harness / humanizer / humanizer-zh / product-planning / session-mechanism)
按授权清空原有内容后重新提交(原 oil-ui-pro 一并移出,可从历史恢复)。 browser-harness 剔除 .venv 等运行环境;根 .gitignore 补记 .venv/ 与 node_modules/。
This commit is contained in:
1 parent
9a152a1952
commit
777f7fe5d0
240 files changed
+52518
-4409
No files matched your search
@@ -0,0 +1,36 @@
|
||||
# shopify-admin
|
||||
|
||||
Browser-harness patterns for `admin.shopify.com` and embedded Shopify apps.
|
||||
|
||||
## Files in this folder
|
||||
|
||||
- `embedded-apps.md` — every Shopify app runs in an iframe; how to target it
|
||||
- `polaris-inputs.md` — Polaris React inputs reject synthetic value setters; use CDP type_text
|
||||
- `knowledge-base.md` — automating the Shopify Knowledge Base App for FAQ entries
|
||||
|
||||
## When to use these
|
||||
|
||||
You're driving Shopify admin and need to add / edit / configure something. The Shopify admin UI is large and many surfaces are embedded apps — first check whether what you need is in an embedded app (most apps under `admin.shopify.com/store/<store>/apps/<app-slug>/...` are).
|
||||
|
||||
## When to skip
|
||||
|
||||
- If the operation is read-only product / inventory data → use the **Storefront API** (HTTP) instead, much faster
|
||||
- If the store has a custom admin app with API token provisioned → use the **Admin API** (GraphQL or REST) instead, no UI scraping
|
||||
- If you're editing theme code → use the **Shopify CLI** (`shopify theme push`) — don't touch the theme editor UI
|
||||
|
||||
The browser is the right tool only when:
|
||||
- The setting / app exposes no API
|
||||
- The change is one-time or rare enough not to justify scripting
|
||||
- You're discovering / exploring the admin (e.g., finding selectors for a future automation)
|
||||
|
||||
## Authentication
|
||||
|
||||
Mike (or the human owner) must be logged into `admin.shopify.com` in the Chrome session that browser-harness attaches to. The harness does NOT log in — it inherits the human's session.
|
||||
|
||||
If you hit `accounts.shopify.com` redirect, stop and ask the human to log in. Don't type credentials.
|
||||
|
||||
## Polaris is in transition (Jan 2026 onward)
|
||||
|
||||
Shopify is migrating its design system from React-based Polaris to Web-Components-based Polaris. Most legacy admin surfaces are still React. Newer surfaces (Catalog Mapping, parts of Settings) may be web components.
|
||||
|
||||
Screenshot first. If you see `<s-text-field>` or `<s-button>` web component tags → use the web component pattern. If you see `[class*="Polaris-"]` React class names → use the CDP keystrokes pattern in `polaris-inputs.md`.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Shopify embedded apps run in iframes
|
||||
|
||||
Every Shopify app surfaced in the admin (first-party like Knowledge Base, third-party like Okendo) renders inside a sandboxed iframe. Your top-level `document` queries find the Shopify chrome (sidebar, header, search bar) but **none of the app's UI**.
|
||||
|
||||
## How to target the iframe
|
||||
|
||||
```python
|
||||
from helpers import iframe_target, js, type_text
|
||||
|
||||
# 1. Find the iframe by URL substring
|
||||
tid = iframe_target("qa-pairs-app") # Knowledge Base App
|
||||
|
||||
# 2. Run JS inside the iframe by passing target_id
|
||||
result = js("""
|
||||
(() => {
|
||||
const button = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Add FAQ');
|
||||
if (button) { button.click(); return {clicked: true}; }
|
||||
return {clicked: false};
|
||||
})()
|
||||
""", target_id=tid)
|
||||
```
|
||||
|
||||
## Finding the URL substring
|
||||
|
||||
The iframe's URL contains the app slug. Run:
|
||||
|
||||
```python
|
||||
import json
|
||||
for t in cdp("Target.getTargets")["targetInfos"]:
|
||||
if t["type"] == "iframe" and "shopify" in t.get("url", "").lower():
|
||||
print(t["url"])
|
||||
```
|
||||
|
||||
Then pick a substring unique to your target app.
|
||||
|
||||
## Known Shopify app iframe slugs
|
||||
|
||||
| App | iframe URL substring |
|
||||
|---|---|
|
||||
| Shopify Knowledge Base (qa-pairs-app) | `qa-pairs-app` |
|
||||
| Shopify Online Store editor | `online-store-web.shopifyapps.com` |
|
||||
| Shopify Hydrogen Storefront | `hydrogen-storefronts` (or similar — verify) |
|
||||
|
||||
Add to this table when you discover new ones.
|
||||
|
||||
## Why iframes
|
||||
|
||||
Shopify uses App Bridge to embed third-party apps with isolation. Your top-level page CAN'T directly access app DOM for security reasons — you need iframe targeting (which the harness does via CDP `Target.attachToTarget`).
|
||||
|
||||
## Coordinate clicks vs JS clicks
|
||||
|
||||
Coordinate clicks (`click(x, y)`) pass through iframes at the compositor level — they work. But JS clicks scoped to the iframe target are more reliable for routine button taps because:
|
||||
|
||||
- Element text content is stable across UI redesigns
|
||||
- DPR scaling on retina is automatic
|
||||
- React event handlers are guaranteed to fire (vs. CDP mouse events which sometimes hit a transparent layer above the button)
|
||||
|
||||
## Gotcha — multiple iframes from same app
|
||||
|
||||
The Online Store editor renders the storefront preview AND the editor toolbar in two separate iframes. Pick the right one by URL substring; don't assume the first match is correct.
|
||||
|
||||
```python
|
||||
# WRONG — picks first match
|
||||
tid = iframe_target("online-store-web")
|
||||
|
||||
# RIGHT — disambiguate
|
||||
for t in cdp("Target.getTargets")["targetInfos"]:
|
||||
url = t.get("url", "")
|
||||
if "online-store-web" in url and "editor" in url:
|
||||
tid = t["targetId"]
|
||||
break
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
# Shopify Knowledge Base App — automating FAQ entries
|
||||
|
||||
The Knowledge Base App (Shopify Winter '26 Edition) lets merchants control how AI agents (ChatGPT, Perplexity, Claude, Copilot, Gemini) answer questions about their brand. Each entry is a Question / Answer pair. The app currently has no public API and is English-only as of Winter '26 — browser automation is the canonical path.
|
||||
|
||||
## URL pattern
|
||||
|
||||
```
|
||||
https://admin.shopify.com/store/<store-handle>/apps/shopify-knowledge-base/app
|
||||
```
|
||||
|
||||
Sub-routes:
|
||||
- `/app` — overview (FAQ list, top unanswered questions, query log)
|
||||
- `/app/new` — Add FAQ form
|
||||
- `/app/pairs/<id>` — entry detail / edit
|
||||
|
||||
## Iframe slug
|
||||
|
||||
The app runs at iframe URL containing `qa-pairs-app`:
|
||||
|
||||
```python
|
||||
tid = iframe_target("qa-pairs-app")
|
||||
```
|
||||
|
||||
## Adding a single FAQ
|
||||
|
||||
See `polaris-inputs.md` for the full canonical pattern. Quick version:
|
||||
|
||||
```python
|
||||
def add_faq(question, answer):
|
||||
tid = iframe_target("qa-pairs-app")
|
||||
# focus question input via JS, type via CDP, focus answer, type, click Save
|
||||
# poll URL for /pairs/<id> success signal
|
||||
```
|
||||
|
||||
## Batching multiple FAQs
|
||||
|
||||
After saving an entry, the success page shows "FAQ created. Add another FAQ" link. Click it via JS to skip navigating back to overview:
|
||||
|
||||
```python
|
||||
def click_add_another():
|
||||
tid = iframe_target("qa-pairs-app")
|
||||
js("""
|
||||
(() => {
|
||||
const link = Array.from(document.querySelectorAll('a, button'))
|
||||
.find(x => x.textContent.trim() === 'Add another FAQ');
|
||||
if (link) link.click();
|
||||
})()
|
||||
""", target_id=tid)
|
||||
```
|
||||
|
||||
Loop:
|
||||
|
||||
```python
|
||||
ENTRIES = [(q1, a1), (q2, a2), ...]
|
||||
for q, a in ENTRIES:
|
||||
click_add_another()
|
||||
time.sleep(1.5) # wait for form to render
|
||||
ok, info = add_faq(q, a)
|
||||
print(f"{q[:40]} -> {ok} ({info})")
|
||||
if not ok: break
|
||||
```
|
||||
|
||||
## Brand voice — what to put in answers
|
||||
|
||||
This is application-specific (depends on the merchant). For JING the rule was Aesop founder-letter tone — sentence case, no exclamation points, "JING" not "we", specific over generic.
|
||||
|
||||
The Shopify guidance "Provide a brief answer in 1 or 2 sentences" is a soft hint. The textarea accepts longer text and AI agents prefer specific multi-sentence answers. Aim for 2-4 short sentences with concrete details.
|
||||
|
||||
## What to put in the Knowledge Base
|
||||
|
||||
Categories that materially shape AI agent answers about your brand:
|
||||
|
||||
1. **Brand voice / DNA** — "What is your brand?" / "What's your tone?"
|
||||
2. **Specs** — exact materials, dimensions, weights, sizes (NOT marketing prose)
|
||||
3. **Comparisons** — "How does X compare to <competitor>?" with concrete differences
|
||||
4. **Policies** — returns, shipping, care, warranty, contact (in brand voice)
|
||||
5. **Origin** — founder, where made, why brand exists
|
||||
6. **Limitations** — what you DON'T do (V1 scope, US-only, etc.) — agents that hallucinate availability hurt conversion
|
||||
|
||||
Skip: anything marketing-speak. The Knowledge Base is for **truth, in voice**, not pitch copy.
|
||||
|
||||
## Top unanswered questions
|
||||
|
||||
The overview shows up to 7 "Top unanswered questions" Shopify auto-detected from query logs. **Answer these first** — they're real shopper queries hitting your store right now. Once answered, the section empties.
|
||||
|
||||
## Query log
|
||||
|
||||
`/admin/apps/shopify-knowledge-base/app/queries` (or "Query log" in app sidebar) shows what shoppers actually asked AI agents about your brand. Read weekly. New patterns become new FAQ entries.
|
||||
|
||||
## Verifying entries surface in AI
|
||||
|
||||
After adding an entry, allow 24 hours for AI provider indexing, then test:
|
||||
|
||||
- ChatGPT: "Tell me about <your brand>'s return policy" → check if your exact wording surfaces
|
||||
- Perplexity: same
|
||||
- Claude: "Compare <your brand> vs <competitor>" → see if your comparison framing appears
|
||||
|
||||
If the answer doesn't surface, the entry might be too long, too vague, or contradicted by another source (your homepage, an outdated blog post). Tighten the answer.
|
||||
|
||||
## Limits
|
||||
|
||||
As of Winter '26 Edition:
|
||||
- English-only
|
||||
- No bulk import / CSV upload
|
||||
- No API for read or write
|
||||
- Each entry maximum ~500 words (soft cap; UI shows guidance "1 or 2 sentences")
|
||||
- No version history visible to the merchant
|
||||
|
||||
Watch Shopify changelogs for API exposure — likely in Spring '26 or Summer '26 Edition. When it ships, switch to API-driven population.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Polaris React inputs require CDP-native keystrokes
|
||||
|
||||
Shopify admin uses Polaris (their design system). Until January 2026 it was React-based. Polaris React text inputs and textareas are controlled components that **reject the standard "React-friendly" synthetic value setter pattern.**
|
||||
|
||||
## The trap
|
||||
|
||||
This pattern looks like it works — the field's `value` shows the right text:
|
||||
|
||||
```js
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
||||
setter.call(inputEl, "my text");
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
```
|
||||
|
||||
But the **Save / Submit button stays disabled**. Polaris's onChange handler reads from React's internal state, which the synthetic event chain doesn't fully update.
|
||||
|
||||
## What works
|
||||
|
||||
CDP-native keystrokes via `Input.insertText`:
|
||||
|
||||
```python
|
||||
from helpers import js, type_text
|
||||
|
||||
# 1. Focus the input via JS — this works fine
|
||||
js("""
|
||||
(() => {
|
||||
const input = Array.from(document.querySelectorAll('input[type="text"], input:not([type])'))
|
||||
.find(x => { const r = x.getBoundingClientRect(); return r.width > 100 && r.height > 0; });
|
||||
if (input) input.focus();
|
||||
})()
|
||||
""", target_id=tid)
|
||||
|
||||
# 2. Type via CDP — fires Input.insertText which is the lowest-level
|
||||
# text-entry signal. React's controlled-input subscriber catches this.
|
||||
type_text("My question text")
|
||||
```
|
||||
|
||||
For textareas, same pattern with `document.querySelectorAll('textarea')`.
|
||||
|
||||
## Full add-FAQ pattern (Knowledge Base App)
|
||||
|
||||
```python
|
||||
import time
|
||||
from helpers import iframe_target, js, type_text, page_info, screenshot
|
||||
|
||||
def add_faq(question: str, answer: str) -> tuple[bool, str]:
|
||||
tid = iframe_target("qa-pairs-app")
|
||||
|
||||
# 1. Make sure the form is rendered
|
||||
for _ in range(15):
|
||||
ready = js("""
|
||||
(() => {
|
||||
const i = Array.from(document.querySelectorAll('input[type="text"], input:not([type])'))
|
||||
.find(x => { const r = x.getBoundingClientRect(); return r.width > 100; });
|
||||
const t = Array.from(document.querySelectorAll('textarea'))
|
||||
.find(x => { const r = x.getBoundingClientRect(); return r.width > 100; });
|
||||
if (i && t) { i.focus(); return true; }
|
||||
return false;
|
||||
})()
|
||||
""", target_id=tid)
|
||||
if ready: break
|
||||
time.sleep(0.3)
|
||||
|
||||
# 2. Type question (input has focus from step 1)
|
||||
type_text(question)
|
||||
time.sleep(0.2)
|
||||
|
||||
# 3. Focus textarea, type answer
|
||||
js("""
|
||||
(() => {
|
||||
const t = Array.from(document.querySelectorAll('textarea'))
|
||||
.find(x => { const r = x.getBoundingClientRect(); return r.width > 100; });
|
||||
if (t) t.focus();
|
||||
})()
|
||||
""", target_id=tid)
|
||||
time.sleep(0.2)
|
||||
type_text(answer)
|
||||
time.sleep(0.4)
|
||||
|
||||
# 4. Click Save (now enabled because Polaris saw real keystrokes)
|
||||
saved = js("""
|
||||
(() => {
|
||||
const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Save');
|
||||
if (!btn || btn.disabled) return {clicked: false, disabled: btn?.disabled};
|
||||
btn.click();
|
||||
return {clicked: true};
|
||||
})()
|
||||
""", target_id=tid)
|
||||
if not saved.get("clicked"):
|
||||
return False, "save_button_disabled"
|
||||
|
||||
# 5. Poll URL for save success — Shopify redirects to /pairs/<id>
|
||||
for _ in range(20):
|
||||
time.sleep(0.3)
|
||||
url = page_info().get("url", "")
|
||||
if "/pairs/" in url and "/new" not in url:
|
||||
return True, url.split("/pairs/")[-1]
|
||||
return False, "save_timeout"
|
||||
```
|
||||
|
||||
## Why this works
|
||||
|
||||
Polaris React components subscribe to native `inputType` events (e.g., `insertText` from IME / accessibility tools / paste). The synthetic React-friendly setter fires `input` events but skips the lower-level `inputType` signal that Polaris validates against to enable Save buttons.
|
||||
|
||||
CDP `Input.insertText` (which the harness's `type_text()` calls) emits the full native event chain, including `inputType: 'insertText'`, which React catches via its synthetic event system.
|
||||
|
||||
## Polaris Web Components (post January 2026)
|
||||
|
||||
The `polaris-react` repo was archived January 6, 2026. New Polaris is web-component-based. For new admin surfaces (Catalog Mapping, parts of Settings), the pattern shifts:
|
||||
|
||||
```js
|
||||
// Web components expose value setter on the element itself
|
||||
const wc = document.querySelector('s-text-field');
|
||||
wc.value = 'my text';
|
||||
wc.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: 'my text' } }));
|
||||
```
|
||||
|
||||
But until Shopify completes the migration (probably late 2026), **always test the React pattern first** — most legacy surfaces still use it.
|
||||
|
||||
## How to know which pattern to use
|
||||
|
||||
Screenshot the form first. Then JS-introspect:
|
||||
|
||||
```js
|
||||
// Check if React-based (Polaris-* class names) or web-component-based (s-* tags)
|
||||
const hasReact = document.querySelector('[class*="Polaris-"]');
|
||||
const hasWC = document.querySelector('s-text-field, s-button, s-textarea');
|
||||
return { hasReact: !!hasReact, hasWC: !!hasWC };
|
||||
```
|
||||
|
||||
If both, lean web component (the surface is mid-migration and the WC will be authoritative).
|
||||
|
||||
## Avoid
|
||||
|
||||
- Coordinate-based typing via `Input.dispatchKeyEvent` keypress-by-keypress — slower, more brittle, no real benefit over `Input.insertText`
|
||||
- `el.value = 'x'` without the setter prototype trick — won't even fill the visible field on Polaris React
|
||||
- `dispatchEvent(new Event('change', ...))` only — Polaris listens for `input`, not `change`, on text fields
|
||||
Reference in new issue
Block a user