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.
@@ -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