@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 +1 -1
- package/skills/x/SKILL.md +20 -7
- package/skills/x/scripts/x.py +92 -21
- package/skills/xiaohongshu/SKILL.md +8 -0
- package/skills/xiaohongshu/references/browse.md +9 -6
- package/skills/xiaohongshu/references/interactions.md +6 -6
- package/skills/xiaohongshu/references/login.md +10 -8
- package/skills/xiaohongshu/references/mcp-parity.md +49 -0
- package/skills/xiaohongshu/references/publish.md +10 -8
- package/skills/xiaohongshu/references/reconciliation.md +8 -0
- package/skills/xiaohongshu/tests/test_browser_contract.py +55 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.720.
|
|
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 #
|
|
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
|
|
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": "
|
|
68
|
+
python3 $X whoami --expect GermeyAce
|
|
69
|
+
# → {"id": "...", "screen_name": "GermeyAce", "identity_verified": true, ...}
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
- **
|
|
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.
|
package/skills/x/scripts/x.py
CHANGED
|
@@ -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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
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,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
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.
|
|
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,
|
|
10
|
-
4. If state
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
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.
|
|
13
|
-
3. The user scans locally.
|
|
14
|
-
4.
|
|
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
|
|
25
|
-
4.
|
|
26
|
-
5. Fill
|
|
27
|
-
6.
|
|
28
|
-
7.
|
|
29
|
-
8.
|
|
30
|
-
9.
|
|
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 "
|
|
144
|
-
assert "
|
|
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
|