xapi-to 0.1.18 → 0.1.20
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/LICENSE +21 -0
- package/README.md +258 -10
- package/dist/chunk-TYY6JR6O.js +870 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1256 -590
- package/dist/openai-sandbox-client.d.ts +85 -0
- package/dist/openai-sandbox-client.js +285 -0
- package/examples/openai-agents-sandbox-local.ts +131 -0
- package/examples/sandbox-api-cli-openai.mjs +450 -0
- package/package.json +33 -4
- package/scripts/openai-sandbox-agent-e2e.ts +219 -0
- package/scripts/sandbox-playground-e2e.mjs +463 -0
- package/skills/xapi/SKILL.md +498 -0
- package/skills/xapi/guides/ai.md +200 -0
- package/skills/xapi/guides/ai_gateway.md +263 -0
- package/skills/xapi/guides/crypto.md +197 -0
- package/skills/xapi/guides/douyin.md +297 -0
- package/skills/xapi/guides/google_search.md +194 -0
- package/skills/xapi/guides/linkedin.md +253 -0
- package/skills/xapi/guides/reddit.md +312 -0
- package/skills/xapi/guides/sandbox.md +466 -0
- package/skills/xapi/guides/serper.md +124 -0
- package/skills/xapi/guides/sms.md +186 -0
- package/skills/xapi/guides/tiktok.md +322 -0
- package/skills/xapi/guides/twitter.md +276 -0
- package/skills/xapi/guides/weibo.md +301 -0
- package/skills/xapi/guides/ws_gateway.md +206 -0
- package/skills/xapi/guides/xiaohongshu.md +315 -0
- package/skills/xapi/scripts/download_tweet_videos.sh +125 -0
- package/src/client.ts +664 -0
- package/src/config.ts +160 -0
- package/src/openai-sandbox-client.ts +349 -0
- package/src/sandbox-client.ts +289 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# WebSocket Gateway Guide
|
|
2
|
+
|
|
3
|
+
Use xAPI's WebSocket Gateway for full-duplex, low-latency sessions such as OpenAI Realtime, streaming speech recognition, bidirectional text-to-speech, simultaneous interpretation, and podcast generation.
|
|
4
|
+
|
|
5
|
+
The WebSocket Gateway shares the public `ai.xapi.to` host with the HTTP AI Gateway, but it is a separate protocol surface. An HTTP request continues to use the AI Gateway; a valid WebSocket Upgrade request is routed to the WebSocket Gateway.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Choose the right interface](#choose-the-right-interface)
|
|
10
|
+
- [Public URLs and routing](#public-urls-and-routing)
|
|
11
|
+
- [Authentication](#authentication)
|
|
12
|
+
- [OpenAI Realtime example](#openai-realtime-example)
|
|
13
|
+
- [Browser connections](#browser-connections)
|
|
14
|
+
- [Native protocol endpoints](#native-protocol-endpoints)
|
|
15
|
+
- [Volcengine ASR options](#volcengine-asr-options)
|
|
16
|
+
- [Connection behavior and billing](#connection-behavior-and-billing)
|
|
17
|
+
- [Errors and reconnects](#errors-and-reconnects)
|
|
18
|
+
- [Security](#security)
|
|
19
|
+
|
|
20
|
+
## Choose the right interface
|
|
21
|
+
|
|
22
|
+
Use:
|
|
23
|
+
|
|
24
|
+
- `npx xapi-to call ai.*` for one-off CLI calls with JSON input and output;
|
|
25
|
+
- the HTTP AI Gateway in `guides/ai_gateway.md` for Anthropic/OpenAI-compatible request-response APIs and SSE streaming;
|
|
26
|
+
- the WebSocket Gateway for a persistent, bidirectional session with text, audio, or provider-native binary frames.
|
|
27
|
+
|
|
28
|
+
Do not send a WebSocket request through `npx xapi-to call`. The CLI action envelope (`action_id` / `input`) and HTTP Gateway request bodies do not apply after a WebSocket connection is established.
|
|
29
|
+
|
|
30
|
+
## Public URLs and routing
|
|
31
|
+
|
|
32
|
+
Preferred unified form:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
wss://ai.xapi.to/<endpoint-path>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Current curated production paths include:
|
|
39
|
+
|
|
40
|
+
| Path | Protocol | Typical use |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `/v1/realtime` | OpenAI Realtime GA JSON events | Realtime text and voice |
|
|
43
|
+
| `/v1/asr` | Volcengine ASR binary frames | Streaming speech recognition |
|
|
44
|
+
| `/v1/tts` | Doubao bidirectional TTS binary frames | Streaming text-to-speech |
|
|
45
|
+
| `/v1/ast` | Doubao AST v4 protobuf frames | Simultaneous interpretation |
|
|
46
|
+
| `/v1/podcast` | Doubao podcast binary frames | Long-form podcast generation |
|
|
47
|
+
|
|
48
|
+
The catalog is dynamic. Confirm the path and wire protocol shown by the current xAPI service before building against it.
|
|
49
|
+
|
|
50
|
+
The unified host resolves a connection by exact path. A unique active endpoint is selected directly. If several endpoints share `/v1/realtime`, the unified route prefers the native `openai-realtime` endpoint. It does not currently use `?model=` to select another realtime provider.
|
|
51
|
+
|
|
52
|
+
For a specific third-party service, use its service host when provided:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
wss://<service-slug>.p.xapi.to/<endpoint-path>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This avoids shared-path ambiguity and is required when the desired service uses a provider-native protocol that is not selected by the unified path. Console Try-It and review workflows can also address an endpoint exactly with `?endpoint=<endpoint-id>`.
|
|
59
|
+
|
|
60
|
+
## Authentication
|
|
61
|
+
|
|
62
|
+
Use the same xAPI key as the CLI and HTTP Gateway. Server-side clients should send one of these handshake headers:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
XAPI-Key: <XAPI_KEY>
|
|
66
|
+
Authorization: Bearer <XAPI_KEY>
|
|
67
|
+
x-api-key: <XAPI_KEY>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Example with `wscat`:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
wscat -c "wss://ai.xapi.to/v1/realtime" \
|
|
74
|
+
-H "XAPI-Key: $XAPI_KEY"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The Gateway also accepts `?token=<XAPI_KEY>` or `?xapi-key=<XAPI_KEY>` for clients that cannot set headers. Avoid query authentication for long-lived keys: URLs are commonly retained in browser history, access logs, error reports, and monitoring systems.
|
|
78
|
+
|
|
79
|
+
Authentication is checked before the WebSocket upgrade. Invalid handshakes therefore return an HTTP status instead of opening and immediately closing a socket.
|
|
80
|
+
|
|
81
|
+
## OpenAI Realtime example
|
|
82
|
+
|
|
83
|
+
The unified `/v1/realtime` route speaks the OpenAI Realtime GA JSON event protocol. It is native passthrough: send the same events you would send to the upstream Realtime API, but authenticate with the xAPI key.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import WebSocket from "ws";
|
|
87
|
+
|
|
88
|
+
const ws = new WebSocket("wss://ai.xapi.to/v1/realtime", {
|
|
89
|
+
headers: { "XAPI-Key": process.env.XAPI_KEY },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
ws.on("message", (raw, isBinary) => {
|
|
93
|
+
if (isBinary) return;
|
|
94
|
+
const event = JSON.parse(raw.toString());
|
|
95
|
+
|
|
96
|
+
if (event.type === "session.created") {
|
|
97
|
+
ws.send(JSON.stringify({
|
|
98
|
+
type: "conversation.item.create",
|
|
99
|
+
item: {
|
|
100
|
+
type: "message",
|
|
101
|
+
role: "user",
|
|
102
|
+
content: [{ type: "input_text", text: "Say hello in one sentence." }],
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
105
|
+
ws.send(JSON.stringify({ type: "response.create" }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (event.type === "response.done") {
|
|
109
|
+
console.log(event.response);
|
|
110
|
+
ws.close(1000, "done");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (event.type === "error") console.error(event.error);
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Do not send the retired `OpenAI-Beta: realtime=v1` header. Session settings, audio buffers, tool calls, and response events follow the current OpenAI Realtime GA shape.
|
|
118
|
+
|
|
119
|
+
## Browser connections
|
|
120
|
+
|
|
121
|
+
The browser `WebSocket` API cannot set arbitrary handshake headers. The Gateway accepts an xAPI key or temporary token through a subprotocol entry:
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const temporaryToken = await getTemporaryTokenFromYourBackend();
|
|
125
|
+
const ws = new WebSocket(
|
|
126
|
+
"wss://ai.xapi.to/v1/realtime",
|
|
127
|
+
[`xapi-key.${temporaryToken}`],
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Never embed a long-lived xAPI key in frontend JavaScript. Use the authenticated xAPI Console Try-It flow or your backend to obtain a short-lived token, then pass only that token to the browser. The Console's `POST /api/keys/ws-token` flow mints a temporary token for a WebSocket endpoint; it requires a logged-in entity account and an endpoint ID, and is not authenticated with a normal xAPI key.
|
|
132
|
+
|
|
133
|
+
If a browser integration must use `?token=`, use only a short-lived token and avoid logging the complete URL.
|
|
134
|
+
|
|
135
|
+
## Native protocol endpoints
|
|
136
|
+
|
|
137
|
+
The Gateway forwards frames without translating the application protocol. The selected adapter extracts usage for billing and observability, but the client still has to speak the endpoint's native wire format.
|
|
138
|
+
|
|
139
|
+
| Adapter | Client frames | Important client requirement |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `openai-realtime` | UTF-8 JSON text | Use OpenAI Realtime GA events. |
|
|
142
|
+
| `volcengine-asr` | Binary | Send the Volcengine ASR header/config/audio frame sequence; PCM configuration must match the audio bytes. |
|
|
143
|
+
| `doubao-realtime` | Binary | Use the Doubao end-to-end realtime dialogue protocol through its service host or exact endpoint. |
|
|
144
|
+
| `doubao-tts` | Binary | Use the bidirectional TTS event sequence; audio responses are provider-native frames. |
|
|
145
|
+
| `doubao-ast` | Binary protobuf | Each message follows Doubao AST v4 protobuf framing. |
|
|
146
|
+
| `doubao-podcast` | Binary | Use the Doubao podcast event protocol and complete input metadata. |
|
|
147
|
+
|
|
148
|
+
Do not send JSON copied from the OpenAI Realtime API to a Doubao binary endpoint. The shared `ai.xapi.to` hostname does not imply a shared event schema, and the Gateway does not currently translate OpenAI Realtime events into Doubao events.
|
|
149
|
+
|
|
150
|
+
For binary services, prefer the service's xAPI Try-It client or the provider protocol documentation. `wscat` can prove that a handshake succeeds, but it is not sufficient for a functional ASR, TTS, AST, or podcast test.
|
|
151
|
+
|
|
152
|
+
## Volcengine ASR options
|
|
153
|
+
|
|
154
|
+
The `/v1/asr` adapter exposes these per-session fields in the native config frame's `request` object:
|
|
155
|
+
|
|
156
|
+
| Field | Default | Meaning |
|
|
157
|
+
|---|---:|---|
|
|
158
|
+
| `enable_punc` | `true` | Insert punctuation. |
|
|
159
|
+
| `enable_itn` | `true` | Normalize spoken numbers, dates, and amounts. |
|
|
160
|
+
| `enable_ddc` | `false` | Remove filler words and repeated speech. |
|
|
161
|
+
| `show_utterances` | `false` | Return utterance boundaries and timestamps. |
|
|
162
|
+
| `result_type` | `full` | Use `full` for cumulative text or `single` for incremental fragments. |
|
|
163
|
+
|
|
164
|
+
Audio must be raw PCM, 16-bit, mono, at 16 kHz (default) or 8 kHz. The rate declared in the config frame must exactly match the bytes sent. The ASR `model_name` is fixed by the selected endpoint and is not a caller-selectable option.
|
|
165
|
+
|
|
166
|
+
## Connection behavior and billing
|
|
167
|
+
|
|
168
|
+
- Frames are forwarded as text or binary without changing their order. The maximum accepted WebSocket message payload is currently 4 MiB.
|
|
169
|
+
- The default idle timeout is 120 seconds, but an endpoint can override it. Send valid application traffic and let the WebSocket library answer ping frames automatically.
|
|
170
|
+
- Maximum session duration is endpoint-specific. Reconnect when the application needs a longer conversation.
|
|
171
|
+
- The default per-key limits are 10 concurrent connections and 60 connection attempts per minute; an endpoint can configure lower or higher values.
|
|
172
|
+
- Billing is endpoint-specific: duration, realtime token usage, or input characters. The Gateway can reserve balance at handshake and settles usage when the connection closes.
|
|
173
|
+
- Unlike HTTP AI Gateway routing, a WebSocket session is pinned to one resolved endpoint and upstream. There is no transparent mid-session provider fallback.
|
|
174
|
+
|
|
175
|
+
## Errors and reconnects
|
|
176
|
+
|
|
177
|
+
Handshake failures:
|
|
178
|
+
|
|
179
|
+
| HTTP status | Meaning |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `400` | The selected endpoint is not a valid WebSocket endpoint or its upstream is unavailable by policy. |
|
|
182
|
+
| `401` | API key is missing, invalid, or expired. |
|
|
183
|
+
| `402` | Balance is insufficient for the initial reservation. |
|
|
184
|
+
| `404` | No active endpoint matches the host, path, or explicit endpoint ID. |
|
|
185
|
+
| `429` | Per-key concurrency or connection-rate limit was exceeded. |
|
|
186
|
+
|
|
187
|
+
After upgrade, important close codes include:
|
|
188
|
+
|
|
189
|
+
| Close code | Meaning |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `1000` | Normal close, idle timeout, or configured maximum duration. |
|
|
192
|
+
| `1001` | Gateway is updating or draining; reconnect after a delay. |
|
|
193
|
+
| `1011` | Upstream connection, timeout, backpressure, or internal gateway failure. |
|
|
194
|
+
| `4401` | The key expired or was revoked while the session was open. |
|
|
195
|
+
| `4402` | Available balance was exhausted during the session. |
|
|
196
|
+
|
|
197
|
+
Reconnect only for recoverable conditions such as `1001`, transient `1011`, or a failed handshake caused by rate limiting. Use exponential backoff with jitter, cap the delay, and stop retrying on authentication, balance, or endpoint errors until the underlying problem is corrected. Recreate session state after reconnect; the Gateway does not resume prior provider sessions.
|
|
198
|
+
|
|
199
|
+
Use a real WebSocket client to test connectivity. A hand-written `curl` Upgrade request can be changed by an intermediary and produce a misleading HTTP response.
|
|
200
|
+
|
|
201
|
+
## Security
|
|
202
|
+
|
|
203
|
+
- Send xAPI credentials only to `*.xapi.to` hosts.
|
|
204
|
+
- Prefer handshake headers for server-side clients and short-lived tokens for browsers.
|
|
205
|
+
- Never put a long-lived key in source code, frontend bundles, query strings, screenshots, or logs.
|
|
206
|
+
- Treat endpoint-specific audio, transcripts, prompts, and generated media as sensitive application data.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# 小红书 (Xiaohongshu / RedNote) Guide
|
|
2
|
+
|
|
3
|
+
Complete guide for Xiaohongshu (小红书) operations via xAPI — user profiles, notes, comments, search, topics, products, and creator inspiration.
|
|
4
|
+
|
|
5
|
+
> **Dynamic catalog:** These are database-registered third-party APIs under the `xiaohongshu` service. Exact action IDs, HTTP methods, parameters, and response fields can change. Run `search` and `get` before calling; the current schema wins. Examples below reflect one known GET-based version and keep `"method":"GET"` in the input for compatibility.
|
|
6
|
+
|
|
7
|
+
**Tip:** Many endpoints accept both a direct ID (e.g. `user_id`, `note_id`) and a `share_text` (share link). You can use either one.
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [User data](#user-data)
|
|
12
|
+
- [Note data](#note-data)
|
|
13
|
+
- [Search](#search)
|
|
14
|
+
- [Topics](#topics)
|
|
15
|
+
- [Products](#products-商品)
|
|
16
|
+
- [Creator inspiration](#creator-inspiration-创作灵感)
|
|
17
|
+
- [Common workflows](#common-workflows)
|
|
18
|
+
- [Pagination](#pagination-patterns)
|
|
19
|
+
- [API reference](#api-reference)
|
|
20
|
+
- [Error handling](#error-handling)
|
|
21
|
+
|
|
22
|
+
## User Data
|
|
23
|
+
|
|
24
|
+
### Get user info
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__user__info \
|
|
28
|
+
--input '{"method":"GET","params":{"user_id":"<user_id>"}}'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Can also pass `share_text` (share link) instead of `user_id`.
|
|
32
|
+
|
|
33
|
+
### Get user's posted notes
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__user__posted__notes \
|
|
37
|
+
--input '{"method":"GET","params":{"user_id":"<user_id>"}}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Optional parameters:
|
|
41
|
+
- `cursor` — pagination cursor, leave empty for first request; pass last `note_id` from previous response
|
|
42
|
+
- `share_text` — can use share link instead of `user_id`
|
|
43
|
+
|
|
44
|
+
### Get user's faved notes
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__user__faved__notes \
|
|
48
|
+
--input '{"method":"GET","params":{"user_id":"<user_id>"}}'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Optional parameters:
|
|
52
|
+
- `cursor` — pagination cursor, pass last `note_id` from previous response
|
|
53
|
+
- `share_text` — share link alternative
|
|
54
|
+
|
|
55
|
+
## Note Data
|
|
56
|
+
|
|
57
|
+
### Get image note detail
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__image__note__detail \
|
|
61
|
+
--input '{"method":"GET","params":{"note_id":"<note_id>"}}'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use this for image-type notes. Can also pass `share_text` instead of `note_id`.
|
|
65
|
+
|
|
66
|
+
### Get video note detail
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__video__note__detail \
|
|
70
|
+
--input '{"method":"GET","params":{"note_id":"<note_id>"}}'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use this for video-type notes. Can also pass `share_text` instead of `note_id`.
|
|
74
|
+
|
|
75
|
+
### Get mixed note detail
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__mixed__note__detail \
|
|
79
|
+
--input '{"method":"GET","params":{"note_id":"<note_id>"}}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Auto-detects note type (image or video) from feed. Can also pass `share_text` instead of `note_id`.
|
|
83
|
+
|
|
84
|
+
**Tip:** If you don't know whether a note is image or video, use `get__mixed__note__detail` — it handles both types.
|
|
85
|
+
|
|
86
|
+
### Get note comments
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__note__comments \
|
|
90
|
+
--input '{"method":"GET","params":{"note_id":"<note_id>","index":0}}'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Optional parameters:
|
|
94
|
+
- `index` — comment index, pass `0` for first request
|
|
95
|
+
- `cursor` — pagination cursor, leave empty for first request
|
|
96
|
+
- `sort_strategy` — `default`, `latest_v2` (最新), `like_count` (最热)
|
|
97
|
+
- `share_text` — share link alternative
|
|
98
|
+
|
|
99
|
+
## Search
|
|
100
|
+
|
|
101
|
+
### Search notes
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_search__notes \
|
|
105
|
+
--input '{"method":"GET","params":{"keyword":"咖啡推荐","page":1}}'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Optional parameters:
|
|
109
|
+
- `page` — page number, start from 1
|
|
110
|
+
- `note_type` — `不限` (all), `视频笔记` (video), `普通笔记` (image), `直播笔记` (live)
|
|
111
|
+
- `sort_type` — sort type
|
|
112
|
+
- `time_filter` — `不限` (all), `一天内` (1 day), `一周内` (1 week), `半年内` (6 months)
|
|
113
|
+
- `ai_mode` — `0` = off, `1` = on (AI-enhanced search)
|
|
114
|
+
- `search_id` — for pagination consistency
|
|
115
|
+
- `search_session_id` — for pagination consistency
|
|
116
|
+
|
|
117
|
+
### Search users
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_search__users \
|
|
121
|
+
--input '{"method":"GET","params":{"keyword":"美食博主","page":1}}'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Optional: `page`, `search_id`.
|
|
125
|
+
|
|
126
|
+
### Search images
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_search__images \
|
|
130
|
+
--input '{"method":"GET","params":{"keyword":"壁纸","page":1}}'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Optional: `page`, `search_id`, `word_request_id`, `search_session_id`.
|
|
134
|
+
|
|
135
|
+
### Search groups
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_search__groups \
|
|
139
|
+
--input '{"method":"GET","params":{"keyword":"摄影","page_no":0}}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Optional parameters:
|
|
143
|
+
- `page_no` — page number, start from **0** (not 1)
|
|
144
|
+
- `is_recommend` — `0` = no, `1` = yes
|
|
145
|
+
- `search_id` — for pagination
|
|
146
|
+
|
|
147
|
+
### Search products
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_search__products \
|
|
151
|
+
--input '{"method":"GET","params":{"keyword":"面霜","page":1}}'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Optional: `page`, `search_id`.
|
|
155
|
+
|
|
156
|
+
## Topics
|
|
157
|
+
|
|
158
|
+
### Get topic info
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__topic__info \
|
|
162
|
+
--input '{"method":"GET","params":{"page_id":"<topic_page_id>"}}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Optional: `note_id` — pass when jumping from a specific note to its topic.
|
|
166
|
+
|
|
167
|
+
### Get topic feed
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__topic__feed \
|
|
171
|
+
--input '{"method":"GET","params":{"page_id":"<topic_page_id>","sort":"trend"}}'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Optional parameters:
|
|
175
|
+
- `sort` — `trend` (最热), `time` (最新)
|
|
176
|
+
- `cursor_score` — pagination cursor score for next page
|
|
177
|
+
- `last_note_id` — last note ID from previous page
|
|
178
|
+
- `last_note_ct` — last note create time from previous page
|
|
179
|
+
- `session_id` — keep consistent across pagination
|
|
180
|
+
- `first_load_time` — keep consistent across pagination
|
|
181
|
+
|
|
182
|
+
## Products (商品)
|
|
183
|
+
|
|
184
|
+
### Get product detail
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__product__detail \
|
|
188
|
+
--input '{"method":"GET","params":{"sku_id":"<sku_id>"}}'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Get product reviews
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__product__reviews \
|
|
195
|
+
--input '{"method":"GET","params":{"sku_id":"<sku_id>","page":0}}'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Optional parameters:
|
|
199
|
+
- `page` — page number, start from **0**
|
|
200
|
+
- `sort_strategy_type` — `0` = general, `1` = latest
|
|
201
|
+
- `share_pics_only` — `0` = all reviews, `1` = only reviews with images
|
|
202
|
+
|
|
203
|
+
### Get product review overview
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__product__review__overview \
|
|
207
|
+
--input '{"method":"GET","params":{"sku_id":"<sku_id>"}}'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Optional: `tab`.
|
|
211
|
+
|
|
212
|
+
### Get product recommendations
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__product__recommendations \
|
|
216
|
+
--input '{"method":"GET","params":{"sku_id":"<sku_id>"}}'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Optional: `cursor_score` for pagination, `region`.
|
|
220
|
+
|
|
221
|
+
## Creator Inspiration (创作灵感)
|
|
222
|
+
|
|
223
|
+
### Get creator inspiration feed
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__creator__inspiration__feed \
|
|
227
|
+
--input '{"method":"GET","params":{}}'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Optional parameters:
|
|
231
|
+
- `tab` — tab type
|
|
232
|
+
- `cursor` — pagination cursor
|
|
233
|
+
|
|
234
|
+
### Get creator hot inspiration feed
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npx xapi-to call xiaohongshu.api_v1_xiaohongshu_app__v2_get__creator__hot__inspiration__feed \
|
|
238
|
+
--input '{"method":"GET","params":{}}'
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Optional: `cursor` for pagination.
|
|
242
|
+
|
|
243
|
+
## Common Workflows
|
|
244
|
+
|
|
245
|
+
### Research a Xiaohongshu user
|
|
246
|
+
|
|
247
|
+
1. Get user info: `xiaohongshu...get__user__info` with `user_id` → profile, follower stats
|
|
248
|
+
2. Get posted notes: `xiaohongshu...get__user__posted__notes` → browse content
|
|
249
|
+
3. Get faved notes: `xiaohongshu...get__user__faved__notes` → what they like
|
|
250
|
+
|
|
251
|
+
### Analyze a note
|
|
252
|
+
|
|
253
|
+
1. Get note detail: `xiaohongshu...get__mixed__note__detail` with `note_id` → full note data
|
|
254
|
+
2. Get comments: `xiaohongshu...get__note__comments` with `sort_strategy=like_count` → top comments
|
|
255
|
+
|
|
256
|
+
### Discover content by topic
|
|
257
|
+
|
|
258
|
+
1. Get topic info: `xiaohongshu...get__topic__info` with `page_id` → topic metadata
|
|
259
|
+
2. Get topic feed: `xiaohongshu...get__topic__feed` with `sort=trend` → trending notes in topic
|
|
260
|
+
|
|
261
|
+
### Research a product
|
|
262
|
+
|
|
263
|
+
1. Get product detail: `xiaohongshu...get__product__detail` with `sku_id` → product info
|
|
264
|
+
2. Get review overview: `xiaohongshu...get__product__review__overview` → rating summary
|
|
265
|
+
3. Get reviews: `xiaohongshu...get__product__reviews` → detailed reviews
|
|
266
|
+
4. Get recommendations: `xiaohongshu...get__product__recommendations` → similar products
|
|
267
|
+
|
|
268
|
+
### Search content
|
|
269
|
+
|
|
270
|
+
1. Search notes: `xiaohongshu...search__notes` with `keyword` → find relevant notes
|
|
271
|
+
2. Search users: `xiaohongshu...search__users` → find creators
|
|
272
|
+
3. Search products: `xiaohongshu...search__products` → find products
|
|
273
|
+
|
|
274
|
+
## Pagination Patterns
|
|
275
|
+
|
|
276
|
+
| Pattern | Endpoints | How to use |
|
|
277
|
+
|---------|-----------|------------|
|
|
278
|
+
| `cursor` (note_id) | user posted/faved notes | Pass last `note_id` from previous response |
|
|
279
|
+
| `cursor` | comments, creator feed, product recommendations | Pass `cursor` from previous response |
|
|
280
|
+
| `page` (from 1) | search notes/users/images/products | Increment `page` by 1 |
|
|
281
|
+
| `page_no` (from 0) | search groups | Increment `page_no` by 1 |
|
|
282
|
+
| `page` (from 0) | product reviews | Increment `page` by 1 |
|
|
283
|
+
| `cursor_score` + `last_note_id` | topic feed | Pass both from previous response |
|
|
284
|
+
|
|
285
|
+
## API Reference
|
|
286
|
+
|
|
287
|
+
| API (prefix: `xiaohongshu.api_v1_xiaohongshu_app__v2_`) | Description | Key Params |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `get__user__info` | User profile | `user_id` or `share_text` |
|
|
290
|
+
| `get__user__posted__notes` | User's notes | `user_id`, `cursor` |
|
|
291
|
+
| `get__user__faved__notes` | User's favorites | `user_id`, `cursor` |
|
|
292
|
+
| `get__image__note__detail` | Image note detail | `note_id` or `share_text` |
|
|
293
|
+
| `get__video__note__detail` | Video note detail | `note_id` or `share_text` |
|
|
294
|
+
| `get__mixed__note__detail` | Mixed note detail (auto-detect) | `note_id` or `share_text` |
|
|
295
|
+
| `get__note__comments` | Note comments | `note_id`, `index`, `cursor`, `sort_strategy` |
|
|
296
|
+
| `search__notes` | Search notes | `keyword`*, `page`, `note_type`, `time_filter` |
|
|
297
|
+
| `search__users` | Search users | `keyword`*, `page` |
|
|
298
|
+
| `search__images` | Search images | `keyword`*, `page` |
|
|
299
|
+
| `search__groups` | Search groups | `keyword`*, `page_no` |
|
|
300
|
+
| `search__products` | Search products | `keyword`*, `page` |
|
|
301
|
+
| `get__topic__info` | Topic info | `page_id`* |
|
|
302
|
+
| `get__topic__feed` | Topic feed | `page_id`*, `sort`, `cursor_score` |
|
|
303
|
+
| `get__product__detail` | Product detail | `sku_id`* |
|
|
304
|
+
| `get__product__reviews` | Product reviews | `sku_id`*, `page`, `sort_strategy_type` |
|
|
305
|
+
| `get__product__review__overview` | Product review overview | `sku_id`* |
|
|
306
|
+
| `get__product__recommendations` | Product recommendations | `sku_id`*, `cursor_score` |
|
|
307
|
+
| `get__creator__inspiration__feed` | Creator inspiration feed | `tab`, `cursor` |
|
|
308
|
+
| `get__creator__hot__inspiration__feed` | Creator hot inspiration | `cursor` |
|
|
309
|
+
|
|
310
|
+
## Error Handling
|
|
311
|
+
|
|
312
|
+
- **Missing note_id** → Use `search__notes` to find notes, or parse `note_id` from share links
|
|
313
|
+
- **Image vs Video note** → If unsure, use `get__mixed__note__detail` which auto-detects
|
|
314
|
+
- **Pagination: page starts from 0 or 1** → Check endpoint docs: search notes/users/images start from `1`, search groups and product reviews start from `0`
|
|
315
|
+
- **Empty results** → Verify the `user_id`, `note_id`, or `sku_id` is valid
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
echo "Usage: $0 <tweet-url-or-id> [output-directory]" >&2
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
die() {
|
|
9
|
+
echo "Error: $*" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for required_command in npx jq curl file mktemp; do
|
|
14
|
+
command -v "$required_command" >/dev/null 2>&1 ||
|
|
15
|
+
die "required command not found: $required_command"
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
TWEET_INPUT=${1:-}
|
|
19
|
+
OUTPUT_DIR=${2:-.}
|
|
20
|
+
[ -n "$TWEET_INPUT" ] || {
|
|
21
|
+
usage
|
|
22
|
+
exit 2
|
|
23
|
+
}
|
|
24
|
+
[ -d "$OUTPUT_DIR" ] || die "output directory does not exist: $OUTPUT_DIR"
|
|
25
|
+
OUTPUT_DIR=$(cd "$OUTPUT_DIR" && pwd -P)
|
|
26
|
+
|
|
27
|
+
if [[ "$TWEET_INPUT" =~ ^https://((www|mobile)\.)?(x\.com|twitter\.com)/[^/]+/status/([0-9]+) ]]; then
|
|
28
|
+
TWEET_ID=${BASH_REMATCH[4]}
|
|
29
|
+
elif [[ "$TWEET_INPUT" =~ ^[0-9]+$ ]]; then
|
|
30
|
+
TWEET_ID=$TWEET_INPUT
|
|
31
|
+
else
|
|
32
|
+
die "expected a numeric tweet ID or an x.com/twitter.com status URL"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
SCHEMA=$(npx xapi-to get twitter.tweet_detail --format json)
|
|
36
|
+
printf '%s' "$SCHEMA" | jq -e '
|
|
37
|
+
.output.properties.data.properties.tweet.properties.media
|
|
38
|
+
.items.properties.video_url.type == "string"
|
|
39
|
+
' >/dev/null || die "twitter.tweet_detail video_url is not deployed on this xAPI backend"
|
|
40
|
+
|
|
41
|
+
INPUT=$(jq -nc --arg tweet_id "$TWEET_ID" '{tweet_id:$tweet_id}')
|
|
42
|
+
RESPONSE=$(npx xapi-to call twitter.tweet_detail --input "$INPUT" --format json)
|
|
43
|
+
|
|
44
|
+
DOWNLOADS=$(printf '%s' "$RESPONSE" | jq -cer '
|
|
45
|
+
def tweet_tree:
|
|
46
|
+
.,
|
|
47
|
+
(
|
|
48
|
+
(.quoted_tweet?, .retweeted_tweet?)
|
|
49
|
+
| select(type == "object")
|
|
50
|
+
| tweet_tree
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if .success != true then
|
|
54
|
+
error("xAPI call failed")
|
|
55
|
+
elif (.data.tweet | type) != "object" then
|
|
56
|
+
error("tweet detail returned no main tweet")
|
|
57
|
+
else
|
|
58
|
+
[
|
|
59
|
+
.data.tweet
|
|
60
|
+
| tweet_tree
|
|
61
|
+
| .media[]?
|
|
62
|
+
| select(.type == "video" or .type == "animated_gif")
|
|
63
|
+
| select((.video_url? | type) == "string" and (.video_url | length) > 0)
|
|
64
|
+
| {url: .video_url}
|
|
65
|
+
]
|
|
66
|
+
| unique_by(.url)
|
|
67
|
+
| if length == 0 then error("tweet has no downloadable MP4") else . end
|
|
68
|
+
end
|
|
69
|
+
')
|
|
70
|
+
|
|
71
|
+
COUNT=$(printf '%s' "$DOWNLOADS" | jq -r 'length')
|
|
72
|
+
URLS=()
|
|
73
|
+
OUTPUTS=()
|
|
74
|
+
TEMP_FILES=()
|
|
75
|
+
|
|
76
|
+
for ((INDEX = 0; INDEX < COUNT; INDEX++)); do
|
|
77
|
+
VIDEO_URL=$(printf '%s' "$DOWNLOADS" | jq -r --argjson index "$INDEX" '.[$index].url')
|
|
78
|
+
[[ "$VIDEO_URL" =~ ^https://video\.twimg\.com/ ]] ||
|
|
79
|
+
die "refusing unexpected media URL outside https://video.twimg.com/"
|
|
80
|
+
URLS+=("$VIDEO_URL")
|
|
81
|
+
if ((COUNT == 1)); then
|
|
82
|
+
OUTPUT="$OUTPUT_DIR/tweet-$TWEET_ID.mp4"
|
|
83
|
+
else
|
|
84
|
+
OUTPUT="$OUTPUT_DIR/tweet-$TWEET_ID-$((INDEX + 1)).mp4"
|
|
85
|
+
fi
|
|
86
|
+
[ ! -e "$OUTPUT" ] || die "output already exists: $OUTPUT"
|
|
87
|
+
OUTPUTS+=("$OUTPUT")
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
cleanup() {
|
|
91
|
+
if ((${#TEMP_FILES[@]})); then
|
|
92
|
+
for temporary_file in "${TEMP_FILES[@]}"; do
|
|
93
|
+
[ -z "$temporary_file" ] || rm -f -- "$temporary_file"
|
|
94
|
+
done
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
trap cleanup EXIT
|
|
98
|
+
|
|
99
|
+
for ((INDEX = 0; INDEX < COUNT; INDEX++)); do
|
|
100
|
+
OUTPUT=${OUTPUTS[$INDEX]}
|
|
101
|
+
TEMP_FILE=$(mktemp "$OUTPUT.part.XXXXXX")
|
|
102
|
+
TEMP_FILES+=("$TEMP_FILE")
|
|
103
|
+
|
|
104
|
+
curl --proto '=https' --proto-redir '=https' \
|
|
105
|
+
--fail --location --silent --show-error \
|
|
106
|
+
--output "$TEMP_FILE" "${URLS[$INDEX]}"
|
|
107
|
+
|
|
108
|
+
MIME_TYPE=$(file --brief --mime-type "$TEMP_FILE")
|
|
109
|
+
[ "$MIME_TYPE" = 'video/mp4' ] ||
|
|
110
|
+
die "downloaded content is not video/mp4 (got $MIME_TYPE)"
|
|
111
|
+
|
|
112
|
+
if command -v ffprobe >/dev/null 2>&1; then
|
|
113
|
+
ffprobe -v error "$TEMP_FILE" >/dev/null ||
|
|
114
|
+
die "ffprobe rejected the downloaded MP4"
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
|
|
118
|
+
# Publish files only after every download has passed validation.
|
|
119
|
+
for ((INDEX = 0; INDEX < COUNT; INDEX++)); do
|
|
120
|
+
mv -- "${TEMP_FILES[$INDEX]}" "${OUTPUTS[$INDEX]}"
|
|
121
|
+
TEMP_FILES[$INDEX]=''
|
|
122
|
+
echo "Downloaded: ${OUTPUTS[$INDEX]}"
|
|
123
|
+
done
|
|
124
|
+
|
|
125
|
+
trap - EXIT
|