@acedatacloud/skills 2026.720.0 → 2026.720.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.720.0",
3
+ "version": "2026.720.2",
4
4
  "description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
5
5
  "keywords": [
6
6
  "agent-skills",
package/skills/x/SKILL.md CHANGED
@@ -44,13 +44,14 @@ python3 -c "import twikit" || { echo "sandbox missing twikit; deploy the sandbox
44
44
  # script (re-run this setup at the top of every fresh-shell Bash block below).
45
45
  X="$SKILL_DIR/scripts/x.py"; [ -f "$X" ] || X=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/x.py' 2>/dev/null | head -1)
46
46
  [ -f "$X" ] || { echo "x script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
47
- python3 "$X" whoami # who is logged in
47
+ python3 "$X" whoami # confirm the shipped CLI can read the authenticated account
48
48
  ```
49
49
 
50
50
  ## Read commands (run directly)
51
51
 
52
52
  ```sh
53
- python3 $X whoami # the logged-in account
53
+ python3 $X whoami --expect GermeyAce # verify this exact account
54
+ python3 $X whoami # discover the authenticated account
54
55
  python3 $X search --query "ai agents" --product Latest --limit 20 # Top | Latest | Media
55
56
  python3 $X search-users --query "openai" --limit 10
56
57
  python3 $X timeline --limit 20 # my home timeline
@@ -64,12 +65,20 @@ python3 $X trends --category trending --limit 20 # trending|for-you|
64
65
  ## Verify the connection first
65
66
 
66
67
  ```sh
67
- python3 $X whoami
68
- # → {"id": "...", "screen_name": "...", "followers_count": ...}
68
+ python3 $X whoami --expect GermeyAce
69
+ # → {"id": "...", "screen_name": "GermeyAce", "identity_verified": true, ...}
69
70
  ```
70
71
 
71
- On an auth error the cookie is expired — have the user reconnect at
72
- <https://auth.acedata.cloud/user/connections>. Do **not** loop-retry.
72
+ `whoami` reads the authenticated account's screen name from X account settings,
73
+ then resolves its public profile. `--expect` compares that server-authenticated
74
+ screen name with the required account before any write. This avoids X's
75
+ Cloudflare-blocked `UserByRestId` endpoint without trusting a local identity
76
+ cookie. Always use `--expect` when the user names the required account.
77
+
78
+ On an actual auth error the cookie is expired — have the user reconnect at
79
+ <https://auth.acedata.cloud/user/connections>. A Cloudflare block is different:
80
+ reconnecting cookies does not fix it. Use `--expect` for identity checks; other
81
+ blocked endpoints need `X_PROXY` or the official X API. Do **not** loop-retry.
73
82
 
74
83
  ## Write commands — GATED (dry-run unless trailing `--confirm`)
75
84
 
@@ -102,13 +111,17 @@ python3 $X delete --id 123456 --confirm # delete one of M
102
111
 
103
112
  - **This is the user's real X account.** Confirm before any write — posts are
104
113
  immediate and public.
105
- - **Not E2E-verified** (see the warning above) — expect to validate the first run.
114
+ - **Live writes are not E2E-verified.** Read commands were verified on 2026-07-06;
115
+ validate the first confirmed write carefully.
106
116
  - **twikit is a scraper of X's non-public API.** It can break when X changes its
107
117
  internal endpoints. The bundled script carries an AceDataCloud compatibility
108
118
  patch for the current `ondemand.s` webpack chunk map used to generate
109
119
  transaction IDs. If `Couldn't get KEY_BYTE indices` appears again, report it
110
120
  as X/twikit upstream drift — do NOT ask the user to reconnect cookies for that
111
121
  specific error.
122
+ - X currently returns a dependency error for its dedicated `UserMedia` query.
123
+ The CLI falls back to the normal Tweets timeline and filters media posts
124
+ locally; the response includes a `fallback` field when this happens.
112
125
  - **ToS / rate-limit / ban risk.** This acts through the web API, not the
113
126
  official API — high-frequency automation can get the account rate-limited or
114
127
  suspended. Keep volume human-like.
@@ -19,6 +19,7 @@ breakage.
19
19
 
20
20
  Examples:
21
21
  python3 x.py whoami
22
+ python3 x.py whoami --expect GermeyAce
22
23
  python3 x.py search --query "python" --product Latest --limit 20
23
24
  python3 x.py timeline --limit 20
24
25
  python3 x.py user-tweets --user elonmusk --type Tweets --limit 20
@@ -38,7 +39,6 @@ import json
38
39
  import os
39
40
  import re
40
41
  import sys
41
- from urllib.parse import unquote
42
42
 
43
43
  UA = (
44
44
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
@@ -90,11 +90,26 @@ def load_cookie_dict() -> dict:
90
90
  return cookies
91
91
 
92
92
 
93
- def current_user_id_from_cookies() -> str | None:
94
- twid = load_cookie_dict().get("twid", "")
95
- decoded = unquote(twid)
96
- match = re.search(r"(?:^|[&?])u=(\d+)", decoded) or re.search(r"u=(\d+)", decoded)
97
- return match.group(1) if match else None
93
+ def normalize_screen_name(value: str) -> str:
94
+ screen_name = value.strip().lstrip("@")
95
+ if not re.fullmatch(r"[A-Za-z0-9_]{1,15}", screen_name):
96
+ die("--expect must be a valid X screen name (1-15 letters, digits, or underscores)")
97
+ return screen_name
98
+
99
+
100
+ def cloudflare_block_message(error: Exception) -> str | None:
101
+ text = str(error)
102
+ if "Cloudflare" not in text or not any(
103
+ marker in text for marker in ("Sorry, you have been blocked", "Attention Required!")
104
+ ):
105
+ return None
106
+ ray_match = re.search(r"Cloudflare Ray ID:.*?([a-f0-9]{16})", text, re.IGNORECASE | re.DOTALL)
107
+ ray = f" Ray ID: {ray_match.group(1)}." if ray_match else ""
108
+ return (
109
+ "X blocked this automated web-API request at Cloudflare; the login cookies may still be valid."
110
+ f"{ray} For identity checks, use `whoami --expect <screen_name>`; for other blocked endpoints, "
111
+ "configure X_PROXY or use the official X API."
112
+ )
98
113
 
99
114
 
100
115
  def make_client():
@@ -230,10 +245,34 @@ async def resolve_user(client, target: str):
230
245
 
231
246
  # ── read commands ───────────────────────────────────────────────────
232
247
 
233
- async def cmd_whoami(client, _args):
234
- user_id = current_user_id_from_cookies()
235
- u = await client.get_user_by_id(user_id) if user_id else await client.user()
236
- out(fmt_user(u))
248
+ async def cmd_whoami(client, args):
249
+ settings, _ = await client.v11.settings()
250
+ authenticated_screen_name = settings.get("screen_name") if isinstance(settings, dict) else None
251
+ if not isinstance(authenticated_screen_name, str) or not re.fullmatch(
252
+ r"[A-Za-z0-9_]{1,15}", authenticated_screen_name
253
+ ):
254
+ die("X account settings did not return a valid screen name; stopped without performing any write")
255
+ expected = getattr(args, "expect", None)
256
+ if expected:
257
+ expected_screen_name = normalize_screen_name(expected)
258
+ if authenticated_screen_name.casefold() != expected_screen_name.casefold():
259
+ die(
260
+ f"connected X account is @{authenticated_screen_name}, not @{expected_screen_name}; "
261
+ "stopped without performing any write"
262
+ )
263
+ u = await client.get_user_by_screen_name(authenticated_screen_name)
264
+ result = fmt_user(u)
265
+ result.update(
266
+ {
267
+ "identity_verified": True,
268
+ "verification": (
269
+ "authenticated_settings_matches_expected_screen_name"
270
+ if expected
271
+ else "authenticated_settings_screen_name"
272
+ ),
273
+ }
274
+ )
275
+ out(result)
237
276
 
238
277
 
239
278
  async def cmd_search(client, args):
@@ -258,20 +297,42 @@ async def cmd_timeline(client, args):
258
297
 
259
298
  async def cmd_user_tweets(client, args):
260
299
  u = await resolve_user(client, args.user)
261
- tweets = await client.get_user_tweets(u.id, args.type, count=args.limit)
262
- items = list(tweets)[: args.limit]
263
- out({"user": fmt_user(u), "type": args.type,
264
- "count": len(items), "tweets": [fmt_tweet(t) for t in items]})
300
+ fallback = None
301
+ try:
302
+ tweets = await client.get_user_tweets(u.id, args.type, count=args.limit)
303
+ items = list(tweets)[: args.limit]
304
+ except Exception as error:
305
+ media_dependency_error = (
306
+ args.type == "Media"
307
+ and (
308
+ (isinstance(error, KeyError) and error.args == ("code",))
309
+ or "Dependency: Unspecified" in str(error)
310
+ )
311
+ )
312
+ if not media_dependency_error:
313
+ raise
314
+ fallback_count = min(max(args.limit * 3, 40), 100)
315
+ tweets = await client.get_user_tweets(u.id, "Tweets", count=fallback_count)
316
+ items = [tweet for tweet in tweets if bool(getattr(tweet, "media", None))][: args.limit]
317
+ fallback = "Tweets (locally filtered for media)"
318
+ result = {
319
+ "user": fmt_user(u),
320
+ "type": args.type,
321
+ "count": len(items),
322
+ "tweets": [fmt_tweet(t) for t in items],
323
+ }
324
+ if fallback:
325
+ result["fallback"] = fallback
326
+ out(result)
265
327
 
266
328
 
267
329
  async def cmd_tweet(client, args):
268
- try:
269
- tweets = await client.get_tweets_by_ids([args.id])
270
- t = tweets[0] if tweets else None
271
- except Exception:
272
- t = None
330
+ tweets = await client.get_tweets_by_ids([args.id])
331
+ t = tweets[0] if tweets else None
273
332
  if t is None:
274
- t = await client.get_tweet_by_id(args.id)
333
+ die("tweet not found / unavailable")
334
+ if str(getattr(t, "id", "")) != args.id:
335
+ die("tweet id mismatch in X response")
275
336
  out(fmt_tweet(t))
276
337
 
277
338
 
@@ -409,7 +470,8 @@ def build_parser() -> argparse.ArgumentParser:
409
470
  p = argparse.ArgumentParser(prog="x.py", description="X (Twitter) cookie CLI")
410
471
  sub = p.add_subparsers(dest="command", required=True)
411
472
 
412
- sub.add_parser("whoami", help="show the logged-in account")
473
+ sp = sub.add_parser("whoami", help="show or verify the logged-in account")
474
+ sp.add_argument("--expect", help="verify the cookie account matches this @screen_name")
413
475
 
414
476
  sp = sub.add_parser("search", help="search tweets by keyword")
415
477
  sp.add_argument("--query", required=True)
@@ -489,12 +551,21 @@ def main() -> None:
489
551
  except (AccountLocked, AccountSuspended) as e:
490
552
  die(f"account locked/suspended by X: {e}")
491
553
  except TooManyRequests as e:
554
+ cloudflare_message = cloudflare_block_message(e)
555
+ if cloudflare_message:
556
+ die(cloudflare_message)
492
557
  die(f"rate limited by X — wait and retry, or slow down. ({e})")
493
558
  except (NotFound, TweetNotAvailable, UserNotFound, UserUnavailable) as e:
494
559
  die(f"not found / unavailable: {e}")
495
560
  except Forbidden as e:
561
+ cloudflare_message = cloudflare_block_message(e)
562
+ if cloudflare_message:
563
+ die(cloudflare_message)
496
564
  die(f"forbidden by X (content rule, protected account, or ToS): {e}")
497
565
  except TwitterException as e:
566
+ cloudflare_message = cloudflare_block_message(e)
567
+ if cloudflare_message:
568
+ die(cloudflare_message)
498
569
  die(f"X API error: {e}")
499
570
  except Exception as e:
500
571
  # twikit scrapes X's non-public API; a bare error here usually means an
@@ -21,12 +21,19 @@ execution:
21
21
  - tabs
22
22
  - snapshot
23
23
  - screenshot
24
+ - element_info
24
25
  - navigate
25
26
  - click
26
27
  - click_at
28
+ - hover
27
29
  - form_input
30
+ - type_text
31
+ - select_option
32
+ - set_checked
28
33
  - key
29
34
  - scroll
35
+ - scroll_to
36
+ - wait_for
30
37
  - file_upload
31
38
  license: Apache-2.0
32
39
  metadata:
@@ -61,6 +68,7 @@ Read only the reference needed for the current request:
61
68
  | Image, video, long article, schedule, original, visibility, products | [publish](./references/publish.md) |
62
69
  | Like, favorite, comment, reply | [interactions](./references/interactions.md) |
63
70
  | Any uncertain write result or interrupted workflow | [reconciliation](./references/reconciliation.md) |
71
+ | Feature parity, route contracts, and selector diagnostics | [MCP parity](./references/mcp-parity.md) |
64
72
 
65
73
  For publish/search validation and feed-card parsing, use the shipped stdlib-only contract helper:
66
74
 
@@ -15,24 +15,27 @@ Use `parse-feed-snapshot` when a machine-checked card list is useful. Preserve `
15
15
 
16
16
  ## Recommendations
17
17
 
18
- Read the attached home/recommendation page. Scroll in bounded steps and read after each step. Return only the requested number of notes with title, author state, visible engagement, and canonical URL. Stop when enough results are collected, the page repeats, a warning appears, or the user limit is reached.
18
+ Navigate to `https://www.xiaohongshu.com/` (or the attached recommendation page), then wait in 300 ms bounded intervals for feed cards to hydrate, for at most 8 seconds. Read the attached page. Scroll in bounded steps and read after each step. Return only the requested number of notes with title, author state, visible engagement, and canonical URL. Stop when enough results are collected, the page repeats, a warning appears, the 8-second hydration window expires without cards, or the user limit is reached.
19
19
 
20
20
  ## Search and filters
21
21
 
22
22
  1. Normalize `{keyword, filters}` with `normalize-filters` before interacting. Supported filters are sort, note type, publish time, search scope, and location.
23
- 2. Read the current page, open the search control, fill the exact keyword, and submit with a fresh visible control or supported key.
24
- 3. Read the result page. Apply requested filters one at a time using fresh refs and verify each visible selected state.
25
- 4. Return bounded cards. Never invent query tokens, counts, or hidden IDs.
23
+ 2. Navigate to `https://www.xiaohongshu.com/search_result?keyword=<encoded>&source=web_explore_feed`, or use the visible search control when already attached there. Never invent a query token.
24
+ 3. Wait for visible results. To filter, hover the visible `筛选` control, wait for the panel, then choose exact labels one group at a time in this order: sort, note type, publish time, search scope, location. Use the normalized labels from `normalize-filters` rather than positional guesses.
25
+ 4. After every selection, wait for the panel/result state to settle and verify the exact selected label before continuing. If the hover panel closes, reacquire it; never click a stale option ref.
26
+ 5. Return bounded cards. Never invent query tokens, counts, or hidden IDs.
26
27
 
27
28
  ## Detail and comments
28
29
 
29
- Open a note from its fresh result ref or same-origin canonical URL. Read visible text, media labels, author, engagement, and the first visible comment batch. For more comments/replies, expand and scroll in bounded batches, reading after every transition and stopping at the requested limit. This is not a guaranteed full-comment export.
30
+ Open a note from its fresh result ref or same-origin canonical URL. Prefer clicking the fresh feed link so any required page token remains browser-local. Retry navigation at most three times with a short bounded delay, then stop. Read visible text, media labels, author, engagement, and the first visible comment batch.
31
+
32
+ For more comments/replies, first scroll to the comment area. In each bounded round: expand at most three visible `展开 N 条回复` controls, read the count, scroll the last visible comment into view, then scroll about 70% of one viewport. Stop at the requested count, visible end marker, no-comment state, 20 stagnant rounds, warning, or caller limit. Never copy the upstream 500-attempt default into chat automation.
30
33
 
31
34
  Pass the final semantic snapshot to `parse-note-snapshot --note-url <canonical-url>`. Preserve missing, ambiguous, and truncated states instead of filling absent fields. Only nodes with an explicit `comment` role are returned as structured comments; generic list items are never assumed to be comments.
32
35
 
33
36
  ## Profile
34
37
 
35
- Open the fresh author link from a result/detail page. Return visible profile text, followers/following/engagement totals, and bounded recent notes. Do not expose unrelated private account data.
38
+ Open the fresh author link from a result/detail page. For the current account, use the sidebar `我` profile channel rather than fabricating an ID. Return visible profile text, followers/following/engagement totals, and bounded recent notes. Profile notes can be grouped/lazy-loaded; flatten only visibly observed note batches. Do not expose unrelated private account data.
36
39
 
37
40
  Pass the final semantic snapshot to `parse-profile-snapshot --profile-url <canonical-url>` before reporting structured profile data. `visible_metrics` contains explicitly labeled profile metrics. Preserve `visible_counts` separately as unlabeled page counters; never reinterpret them as followers, following, likes, or favorites.
38
41
 
@@ -6,22 +6,22 @@ Always open and read the exact target note first. Derive it from the user's URL
6
6
 
7
7
  1. Read the target control's visible label and checked/pressed/selected state.
8
8
  2. If the current state already matches the explicit request, no-op and report it.
9
- 3. Otherwise click once, read again, and confirm the target state.
10
- 4. If state remains ambiguous, do not retry automatically.
9
+ 3. Otherwise click once, poll the fresh state every 500 ms for at most 4 seconds, and confirm the exact target state.
10
+ 4. If the state is readable and clearly unchanged, one retry is allowed because the action is reversible; never exceed two total clicks. If state cannot be read, do not retry.
11
11
 
12
12
  ## Comment
13
13
 
14
14
  1. Draft the exact comment and show the target note plus full text.
15
15
  2. Obtain explicit chat confirmation.
16
- 3. Open the visible editor, fill it, and read the draft back.
16
+ 3. Open the visible comment placeholder, then fill the visible contenteditable comment input and read the draft back.
17
17
  4. If the target or exact text differs, stop.
18
- 5. Click Send once after the confirmed preview, then follow reconciliation.
18
+ 5. Click the visible submit button once after the confirmed preview. Poll the visible comments for the exact text every 300 ms for at most 4 seconds; absence is an unknown/failed result, not success.
19
19
 
20
20
  ## Reply
21
21
 
22
- 1. Locate the exact visible target comment and author, expanding replies in bounded steps if needed.
22
+ 1. Locate the exact visible target comment and author. Prefer an exact comment ID when visible; otherwise match the explicitly confirmed user. Expand/scroll in bounded steps, stop at the end marker, and never search more than 100 rounds.
23
23
  2. Show the target and full reply preview; obtain explicit confirmation.
24
24
  3. Open that comment's reply control, fill the reply, and read the visible target/text back.
25
- 4. Submit once, then follow reconciliation.
25
+ 4. Submit once, then require the exact reply text to appear within 4 seconds before reporting success.
26
26
 
27
27
  Comments and replies are public account actions. The attached exact-origin session does not replace explicit chat preview confirmation.
@@ -2,21 +2,23 @@
2
2
 
3
3
  ## Status
4
4
 
5
- 1. Attach `https://www.xiaohongshu.com` and call `browser.snapshot` with that exact origin.
6
- 2. Determine login only from visible signed-in controls, profile links, or an explicit login prompt. Do not claim cryptographic account attestation.
7
- 3. If the state is ambiguous or the snapshot is truncated before account controls, ask the user to inspect the tab.
5
+ 1. Navigate or attach `https://www.xiaohongshu.com/explore` and wait up to 30 seconds in bounded intervals.
6
+ 2. Treat a visible sidebar profile channel (`我` / profile link) as signed in. Treat a visible login prompt or QR container as signed out. URL alone is not sufficient unless it visibly redirects to login.
7
+ 3. Determine login only from visible signed-in controls, profile links, or an explicit login prompt. Do not claim cryptographic account attestation.
8
+ 4. If `browser.snapshot` is unavailable on the dense page, use `browser.screenshot` for this status check. If neither proves the state, ask the user to inspect the tab.
8
9
 
9
10
  ## QR login
10
11
 
11
- 1. Open the visible login control with a fresh ref.
12
- 2. Read again. If a QR is present, call `browser.screenshot` and present the image; never extract QR payloads or session tokens.
13
- 3. The user scans locally. Wait in bounded intervals and read again until a visible signed-in state appears or the QR expires.
14
- 4. On expiry, ask before reopening a fresh QR. Never loop indefinitely.
12
+ 1. Open the visible login control with a fresh ref or screenshot-bound point.
13
+ 2. Read again. The upstream page contract renders the QR under the login container; call `browser.screenshot` and present the visible image, never the QR payload or session token.
14
+ 3. The user scans locally. Poll every 500 ms only through bounded `browser.wait_for`/fresh observations, with a maximum matching the visible QR expiry (never more than 5 minutes).
15
+ 4. Success requires the visible sidebar profile channel to appear. A disappeared QR alone is not success.
16
+ 5. On expiry, ask before reopening a fresh QR. Never loop indefinitely.
15
17
 
16
18
  ## Switch or sign out
17
19
 
18
20
  1. Show the visible current-account context and ask for explicit confirmation.
19
- 2. Use visible logout/switch controls with fresh refs after chat confirmation.
21
+ 2. Use visible logout/switch controls with fresh refs after chat confirmation. Do not copy the upstream Cookie-deletion shortcut; this Skill never clears or exports Cookies.
20
22
  3. Let the user complete credentials, SMS, QR, or verification locally.
21
23
  4. Read again and report only the visible resulting account state.
22
24
 
@@ -0,0 +1,49 @@
1
+ # Xiaohongshu MCP feature parity
2
+
3
+ This Skill benchmarks its business workflows against `xpzouying/xiaohongshu-mcp` commit
4
+ `a7d1f2f7f45e0b1c27de67c8f8a19131ba321725`. Reimplement the behavior through generic
5
+ `browser.*` capabilities; never copy its CDP runtime into the extension.
6
+
7
+ | Reference feature | Skill workflow |
8
+ |---|---|
9
+ | `check_login_status` | [login status](./login.md#status) |
10
+ | `get_login_qrcode` | [QR login](./login.md#qr-login) |
11
+ | `publish_content` | [image publish](./publish.md#execute) |
12
+ | `publish_with_video` | [video publish](./publish.md#execute) |
13
+ | long-article pipeline | [long article steps](./publish.md#execute) |
14
+ | `list_feeds` | [recommendations](./browse.md#recommendations) |
15
+ | `search_feeds` + five filter groups | [search and filters](./browse.md#search-and-filters) |
16
+ | `get_feed_detail` + comment loading | [detail and comments](./browse.md#detail-and-comments) |
17
+ | `user_profile` / `get_me` | [profile](./browse.md#profile) |
18
+ | `post_comment_to_feed` | [comment](./interactions.md#comment) |
19
+ | `reply_comment_in_feed` | [reply](./interactions.md#reply) |
20
+ | `like_feed` / unlike | [like and favorite](./interactions.md#like-and-favorite) |
21
+ | `favorite_feed` / unfavorite | [like and favorite](./interactions.md#like-and-favorite) |
22
+
23
+ ## Selector recognition hints
24
+
25
+ These are Xiaohongshu-specific diagnostics owned by this Skill. Use visible semantics first and reacquire
26
+ fresh refs after every transition.
27
+
28
+ - Creator mode tabs: `div.creator-tab`, exact text `上传图文`, `上传视频`, `写长文`.
29
+ - Upload input: `input.upload-input`, fallback single visible/associated `input[type=file]`.
30
+ - Image completion: `.img-preview-area .pr` count.
31
+ - Image/video title: placeholder containing `填写标题`, fallback visible title input.
32
+ - Long-title field: `textarea.d-text[placeholder="输入标题"]`.
33
+ - Body/editor: `div.tiptap.ProseMirror`, fallback visible `div.ProseMirror[contenteditable=true]`.
34
+ - Publish: enabled `xhs-publish-btn`, fallback visible `.publish-page-publish-btn button.bg-red`.
35
+ - Comment opener/input/submit: `div.input-box div.content-edit span`, `p.content-input`, `div.bottom button.submit`.
36
+ - Like/favorite: `.like-lottie`, `.collect-icon`; always verify resulting state.
37
+
38
+ Selectors are recognition hints, not permission to run arbitrary JavaScript or CSS queries. If generic
39
+ browser observations cannot identify a unique visible target, use a screenshot-bound action or stop.
40
+
41
+ ## Deliberate non-parity
42
+
43
+ Do not import upstream behaviors that violate the local-browser security boundary:
44
+
45
+ - no Cookie deletion/export or account state reset;
46
+ - no arbitrary CDP/evaluate, remote debugging port, or headless profile;
47
+ - no local filesystem paths or direct URL downloads in `browser.file_upload`;
48
+ - no hidden `window.__INITIAL_STATE__` extraction; report only visible browser observations;
49
+ - no 500-round unattended comment crawling; all loops are bounded and user-request limited.
@@ -20,13 +20,15 @@ Show the exact normalized preview: post type, title, full body, tags, media name
20
20
  ## Execute
21
21
 
22
22
  1. Ask the user to open `https://creator.xiaohongshu.com`, attach that tab, and locally verify the signed-in account.
23
- 2. Read the page and stop on warnings or unexpected account context.
24
- 3. Select image, video, or long-article mode using fresh refs.
25
- 4. Upload approved resource IDs through `browser.file_upload`, or wait for the user to select local media. Read until exact filenames/thumbnails and processing completion are visible.
26
- 5. Fill title/body using fresh refs. For rich-text editors, read immediately after input; if the exact value is not visible, stop rather than repeatedly injecting it.
27
- 6. Add tags/topics, template/layout, products, visibility, originality, and schedule one at a time. Read and verify each state.
28
- 7. Before the final action, read again and compare every visible field with the confirmed preview. Stop on mismatch.
29
- 8. Click Publish/Schedule exactly once after the final confirmed chat preview.
30
- 9. Follow [reconciliation](./reconciliation.md). Report success only from a visible success state or destination, and include the canonical URL when available.
23
+ 2. Navigate to `https://creator.xiaohongshu.com/publish/publish?source=official`. Wait for load, then allow two seconds for creator widgets and one bounded DOM-settle interval. Read or screenshot the page and stop on warnings, login redirects, or unexpected account context.
24
+ 3. Select mode by exact visible tab text: `上传图文`, `上传视频`, or `写长文`. Prefer a fresh semantic ref; if the tab is visually blocked by a popover, stop for user inspection instead of deleting page nodes. Verify the selected mode after clicking.
25
+ 4. For image posts, upload one approved resource at a time and wait until the visible preview count reaches the submitted count before the next resource (up to 60 seconds per image). For video, wait until processing completes and Publish becomes enabled, up to 10 minutes. If resource resolution is unavailable, wait for the user to select local media and verify the same preview/processing state.
26
+ 5. Fill the image/video title using the visible title textbox (recognition hints: placeholder containing `填写标题`, then the single visible title input fallback). Fill body in the visible TipTap/ProseMirror contenteditable editor. Use `browser.form_input` for replacement or focus with `browser.click`/`click_at` then `browser.type_text` for rich text. Read immediately after each field; stop if the exact normalized value is not visible.
27
+ 6. Limit tags to the first 10 confirmed tags. Insert them one at a time, close any topic suggestion popover by focusing the title, and verify visible chips/text before continuing.
28
+ 7. Configure options one at a time and verify each exact state: schedule (1 hour–14 days), visibility (`公开可见`, `仅自己可见`, `仅互关好友可见`), originality, and products. If originality was requested but cannot be confirmed, abort rather than publishing non-original. Bind a product only when the exact intended product is visibly selected; never accept a first fuzzy match silently.
29
+ 8. For long article: choose `写长文` → `新的创作`; fill `输入标题` textarea and ProseMirror body; click `一键排版`; enumerate visible template names; select the confirmed template and verify its selected state; click `下一步`; then fill the separate publish-page description editor.
30
+ 9. Before the final action, read or screenshot again and compare media count, title, full body, tags, options, products, and schedule with the confirmed preview. Stop on mismatch.
31
+ 10. Locate Publish through two page generations: visible enabled `xhs-publish-btn` widget first, then visible legacy red Publish button. Reject `submit-disabled=true`, `disabled`, `aria-disabled=true`, or disabled styling. Click exactly once after the final confirmed chat preview.
32
+ 11. Follow [reconciliation](./reconciliation.md). Immediate success requires leaving `/publish/publish` or a visible success destination within 15 seconds. Remaining on the form is not success. Return the canonical note URL when visible.
31
33
 
32
34
  Never mix image/video media unless the visible current UI explicitly supports it. Bind products only when the account visibly exposes the feature and the exact selected products appear in the final preview.
@@ -12,4 +12,12 @@ Use this after timeout, disconnect, stale ref, navigation, an ambiguous result,
12
12
  5. If `not_applied`, reversible actions may be attempted once from the verified state. Irreversible actions require a fresh preview and renewed chat confirmation.
13
13
  6. If `unknown`, stop and ask the user to inspect the local tab. Never retry publish, schedule, comment, or reply while unknown.
14
14
 
15
+ Action-specific success signals benchmarked from the local-browser workflow:
16
+
17
+ - Image upload: preview count reaches the submitted image count; a file-input event alone is not success.
18
+ - Video upload: Publish becomes enabled after processing; file selection alone is not success.
19
+ - Publish/schedule: URL leaves `/publish/publish` or a visible success destination appears within 15 seconds.
20
+ - Comment/reply: the exact submitted text renders in the comments area within 4 seconds.
21
+ - Like/favorite: the visible/semantic state equals the requested boolean; click completion alone is not success.
22
+
15
23
  Preserve the page on CAPTCHA, moderation, rate limit, account restriction, or unusual-activity warnings. Do not dismiss, solve, bypass, or retry around them.
@@ -12,6 +12,7 @@ REFERENCES = {
12
12
  "publish.md",
13
13
  "interactions.md",
14
14
  "reconciliation.md",
15
+ "mcp-parity.md",
15
16
  }
16
17
  EXPECTED_ORIGINS = {
17
18
  "https://www.xiaohongshu.com",
@@ -21,12 +22,19 @@ EXPECTED_CAPABILITIES = {
21
22
  "tabs",
22
23
  "snapshot",
23
24
  "screenshot",
25
+ "element_info",
24
26
  "navigate",
25
27
  "click",
26
28
  "click_at",
29
+ "hover",
27
30
  "form_input",
31
+ "type_text",
32
+ "select_option",
33
+ "set_checked",
28
34
  "key",
29
35
  "scroll",
36
+ "scroll_to",
37
+ "wait_for",
30
38
  "file_upload",
31
39
  }
32
40
  DEPLOYED_BROWSER_TOOLS = {
@@ -140,8 +148,8 @@ def test_browser_skill_matches_complete_local_runtime() -> None:
140
148
  assert "ask the user to open `https://creator.xiaohongshu.com`" in text
141
149
  assert "opaque resource ids" in text
142
150
  assert "trusted_input" not in _nested_list(_frontmatter(SKILL.read_text(encoding="utf-8")), "capabilities")
143
- assert "browser.read_page" not in text
144
- assert "browser.wait" not in text
151
+ assert not re.search(r"\bbrowser\.read_page\b", text)
152
+ assert not re.search(r"\bbrowser\.wait\b", text)
145
153
  assert "local approval" not in text
146
154
  assert "publish image, video, or long-article notes" in text
147
155
  assert "long_article" in text
@@ -164,4 +172,48 @@ def test_browser_skill_matches_complete_local_runtime() -> None:
164
172
  assert "content planning" in text
165
173
  assert "reconciliation after uncertain browser writes" in text
166
174
  assert "stop on warning" in text
167
- assert "explicit confirmation" in text
175
+ assert "explicit confirmation" in text
176
+
177
+
178
+ def test_upstream_mcp_feature_parity_and_skill_ownership() -> None:
179
+ text = "\n".join(
180
+ path.read_text(encoding="utf-8")
181
+ for path in [SKILL, *(SKILL_DIR / "references").glob("*.md")]
182
+ ).casefold()
183
+
184
+ for feature in (
185
+ "check_login_status",
186
+ "get_login_qrcode",
187
+ "publish_content",
188
+ "publish_with_video",
189
+ "list_feeds",
190
+ "search_feeds",
191
+ "get_feed_detail",
192
+ "user_profile",
193
+ "get_me",
194
+ "post_comment_to_feed",
195
+ "reply_comment_in_feed",
196
+ "like_feed",
197
+ "favorite_feed",
198
+ ):
199
+ assert feature in text
200
+
201
+ for invariant in (
202
+ "上传图文",
203
+ "上传视频",
204
+ "写长文",
205
+ "新的创作",
206
+ "一键排版",
207
+ "xhs-publish-btn",
208
+ "submit-disabled=true",
209
+ "img-preview-area",
210
+ "10 minutes",
211
+ "15 seconds",
212
+ "4 seconds",
213
+ "never exceed two total clicks",
214
+ ):
215
+ assert invariant.casefold() in text
216
+
217
+ assert "a7d1f2f7f45e0b1c27de67c8f8a19131ba321725" in text
218
+ assert "no arbitrary cdp/evaluate" in text
219
+ assert "no cookie deletion/export" in text