@lucasygu/redbook 0.3.3 → 0.5.0
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/README.md +54 -8
- package/SKILL.md +38 -4
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +119 -17
- package/dist/cli.js.map +1 -1
- package/dist/lib/cdp-cookies.d.ts +28 -0
- package/dist/lib/cdp-cookies.d.ts.map +1 -0
- package/dist/lib/cdp-cookies.js +256 -0
- package/dist/lib/cdp-cookies.js.map +1 -0
- package/dist/lib/client.d.ts +3 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +24 -0
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/cookies.d.ts +11 -1
- package/dist/lib/cookies.d.ts.map +1 -1
- package/dist/lib/cookies.js +138 -17
- package/dist/lib/cookies.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,9 +24,12 @@ npm install -g @lucasygu/redbook
|
|
|
24
24
|
clawhub install redbook
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
需要 Node.js >= 22
|
|
27
|
+
需要 Node.js >= 22。支持 **macOS、Windows、Linux**。使用 Chrome 浏览器的 Cookie —— 请先在 Chrome 中登录 xiaohongshu.com。
|
|
28
28
|
|
|
29
|
-
安装后运行 `redbook whoami`
|
|
29
|
+
安装后运行 `redbook whoami` 验证连接。CLI 会自动检测所有 Chrome 配置文件,找到你的小红书登录状态。
|
|
30
|
+
|
|
31
|
+
- **macOS** —— 如果遇到钥匙串弹窗,请点击"始终允许"
|
|
32
|
+
- **Windows** —— Chrome 127+ 使用了 App-Bound Encryption,CLI 会自动启动 Chrome headless 模式读取 Cookie(需要先关闭 Chrome)。如果自动提取失败,可以用 `--cookie-string` 手动传入
|
|
30
33
|
|
|
31
34
|
## 能做什么
|
|
32
35
|
|
|
@@ -34,6 +37,7 @@ clawhub install redbook
|
|
|
34
37
|
- **竞品分析** —— 找到头部博主,对比粉丝量、互动数据、内容风格
|
|
35
38
|
- **爆款拆解** —— 分析爆款笔记的标题钩子、互动比例、评论主题
|
|
36
39
|
- **爆款模板** —— 从多篇爆款笔记提取内容模板(标题结构、正文结构、钩子模式)
|
|
40
|
+
- **收藏管理** —— 查看收藏列表、收藏/取消收藏笔记(支持自己和其他用户的公开收藏)
|
|
37
41
|
- **评论管理** —— 发评论、回复评论、按策略批量回复(问题优先 / 高赞优先 / 未回复优先)
|
|
38
42
|
- **图文卡片** —— Markdown 渲染为小红书风格的 PNG 图文卡片(7 种配色主题)
|
|
39
43
|
- **内容策划** —— 基于数据发现内容机会,生成有数据支撑的选题建议
|
|
@@ -66,6 +70,14 @@ redbook user-posts <userId>
|
|
|
66
70
|
# 搜索话题标签
|
|
67
71
|
redbook topics "Claude Code"
|
|
68
72
|
|
|
73
|
+
# 查看收藏(默认当前用户)
|
|
74
|
+
redbook favorites --json
|
|
75
|
+
redbook favorites <userId> --json --all
|
|
76
|
+
|
|
77
|
+
# 收藏/取消收藏
|
|
78
|
+
redbook collect "<noteUrl>"
|
|
79
|
+
redbook uncollect "<noteUrl>"
|
|
80
|
+
|
|
69
81
|
# 分析爆款笔记
|
|
70
82
|
redbook analyze-viral https://www.xiaohongshu.com/explore/abc123
|
|
71
83
|
|
|
@@ -104,6 +116,9 @@ redbook post --title "测试" --body "..." --images img.png --private
|
|
|
104
116
|
| `feed` | 获取推荐页内容 |
|
|
105
117
|
| `post` | 发布图文笔记(易触发验证码,详见下方说明) |
|
|
106
118
|
| `topics <关键词>` | 搜索话题/标签 |
|
|
119
|
+
| `favorites [userId]` | 查看收藏笔记列表(默认当前用户) |
|
|
120
|
+
| `collect <url>` | 收藏(书签)笔记 |
|
|
121
|
+
| `uncollect <url>` | 取消收藏笔记 |
|
|
107
122
|
| `analyze-viral <url>` | 分析爆款笔记(钩子、互动、结构) |
|
|
108
123
|
| `viral-template <url...>` | 从 1-3 篇爆款笔记提取内容模板 |
|
|
109
124
|
| `comment <url>` | 发表评论 |
|
|
@@ -117,6 +132,7 @@ redbook post --title "测试" --body "..." --images img.png --private
|
|
|
117
132
|
|------|------|--------|
|
|
118
133
|
| `--cookie-source <浏览器>` | Cookie 来源浏览器(chrome, safari, firefox) | `chrome` |
|
|
119
134
|
| `--chrome-profile <名称>` | Chrome 配置文件目录名(如 "Profile 1"),默认自动检测 | 自动 |
|
|
135
|
+
| `--cookie-string <cookies>` | 手动传入 Cookie 字符串:`"a1=值; web_session=值"`(从 Chrome DevTools 复制) | 无 |
|
|
120
136
|
| `--json` | JSON 格式输出 | `false` |
|
|
121
137
|
|
|
122
138
|
### 搜索选项
|
|
@@ -180,13 +196,20 @@ npm install -g puppeteer-core marked
|
|
|
180
196
|
| 问题 | 解决方案 |
|
|
181
197
|
|------|----------|
|
|
182
198
|
| `No 'a1' cookie found` | 在 Chrome 中登录 xiaohongshu.com,然后重试 |
|
|
199
|
+
| Windows 上 `-101` 错误 | Chrome 127+ 的 App-Bound Encryption 导致。先**关闭 Chrome**,再运行命令(CLI 会自动启动 Chrome headless 读取 Cookie)。如仍失败,用 `--cookie-string` 手动传入 |
|
|
200
|
+
| Windows `--cookie-string` 用法 | Chrome 按 F12 → Application → Cookies → xiaohongshu.com,复制 `a1` 和 `web_session` 的值:`redbook whoami --cookie-string "a1=值; web_session=值"` |
|
|
183
201
|
| macOS 钥匙串弹窗 | 输入密码后点击"始终允许",CLI 需要读取 Chrome 的加密 Cookie |
|
|
184
|
-
| 多个 Chrome 配置文件 | CLI
|
|
202
|
+
| 多个 Chrome 配置文件 | CLI 自动扫描所有配置文件(macOS / Windows / Linux)。如需指定:`--chrome-profile "Profile 1"` |
|
|
185
203
|
| 使用 Brave/Arc 等浏览器 | 尝试 `--cookie-source safari`,或在 Chrome 中登录 |
|
|
186
204
|
|
|
187
205
|
## 工作原理
|
|
188
206
|
|
|
189
|
-
`redbook` 从 Chrome 读取小红书的登录 Cookie
|
|
207
|
+
`redbook` 从 Chrome 读取小红书的登录 Cookie,然后用 TypeScript 实现的签名算法对 API 请求签名。
|
|
208
|
+
|
|
209
|
+
**三层 Cookie 提取策略:**
|
|
210
|
+
1. **sweet-cookie**(快速路径)—— 直接读取 Chrome 的 SQLite 数据库,macOS 上即开即用
|
|
211
|
+
2. **CDP 回退**(Windows 自动触发)—— 启动 Chrome headless,通过 DevTools Protocol 读取 Cookie,绕过 Chrome 127+ 的 App-Bound Encryption
|
|
212
|
+
3. **`--cookie-string`**(手动兜底)—— 从 Chrome DevTools 复制 Cookie 字符串,任何平台通用
|
|
190
213
|
|
|
191
214
|
**两套签名系统:**
|
|
192
215
|
- **主 API**(`edith.xiaohongshu.com`)—— 读取:搜索、推荐页、笔记、评论、用户资料。使用 144 字节 x-s 签名(v4.3.1)
|
|
@@ -300,9 +323,12 @@ npm install -g @lucasygu/redbook
|
|
|
300
323
|
clawhub install redbook
|
|
301
324
|
```
|
|
302
325
|
|
|
303
|
-
Requires Node.js >= 22. Uses cookies from your Chrome browser session — you must be logged into xiaohongshu.com in Chrome.
|
|
326
|
+
Requires Node.js >= 22. Supports **macOS, Windows, and Linux**. Uses cookies from your Chrome browser session — you must be logged into xiaohongshu.com in Chrome.
|
|
304
327
|
|
|
305
|
-
After installing, run `redbook whoami` to verify the connection.
|
|
328
|
+
After installing, run `redbook whoami` to verify the connection. The CLI auto-detects all Chrome profiles to find your XHS session.
|
|
329
|
+
|
|
330
|
+
- **macOS** — If Keychain prompt appears, click "Always Allow"
|
|
331
|
+
- **Windows** — Chrome 127+ uses App-Bound Encryption. The CLI auto-launches Chrome headless to read cookies (close Chrome first). If auto-extraction fails, use `--cookie-string` as fallback
|
|
306
332
|
|
|
307
333
|
## What You Can Do
|
|
308
334
|
|
|
@@ -310,6 +336,7 @@ After installing, run `redbook whoami` to verify the connection. If macOS shows
|
|
|
310
336
|
- **Competitive analysis** — Find top creators, compare followers, engagement, content style
|
|
311
337
|
- **Viral note breakdown** — Analyze title hooks, engagement ratios, comment themes
|
|
312
338
|
- **Viral templates** — Extract content templates from multiple viral notes (hook patterns, body structure, engagement profile)
|
|
339
|
+
- **Favorites management** — List collected notes, collect/uncollect notes (own and other users' public collections)
|
|
313
340
|
- **Comment management** — Post comments, reply to comments, batch-reply with strategies (questions / top-engaged / unanswered)
|
|
314
341
|
- **Image cards** — Render markdown to styled PNG cards for XHS posts (7 color themes)
|
|
315
342
|
- **Content planning** — Discover content opportunities with data-backed topic suggestions
|
|
@@ -342,6 +369,14 @@ redbook user-posts <userId>
|
|
|
342
369
|
# Search hashtags
|
|
343
370
|
redbook topics "Claude Code"
|
|
344
371
|
|
|
372
|
+
# List favorites (defaults to current user)
|
|
373
|
+
redbook favorites --json
|
|
374
|
+
redbook favorites <userId> --json --all
|
|
375
|
+
|
|
376
|
+
# Collect/uncollect notes
|
|
377
|
+
redbook collect "<noteUrl>"
|
|
378
|
+
redbook uncollect "<noteUrl>"
|
|
379
|
+
|
|
345
380
|
# Analyze a viral note
|
|
346
381
|
redbook analyze-viral https://www.xiaohongshu.com/explore/abc123
|
|
347
382
|
|
|
@@ -380,6 +415,9 @@ redbook post --title "测试" --body "..." --images img.png --private
|
|
|
380
415
|
| `feed` | Get homepage feed |
|
|
381
416
|
| `post` | Publish an image note (captcha-prone, see below) |
|
|
382
417
|
| `topics <keyword>` | Search for topics/hashtags |
|
|
418
|
+
| `favorites [userId]` | List collected/favorited notes (defaults to current user) |
|
|
419
|
+
| `collect <url>` | Collect (bookmark) a note |
|
|
420
|
+
| `uncollect <url>` | Remove a note from your collection |
|
|
383
421
|
| `analyze-viral <url>` | Analyze why a viral note works (hooks, engagement, structure) |
|
|
384
422
|
| `viral-template <url...>` | Extract a content template from 1-3 viral notes |
|
|
385
423
|
| `comment <url>` | Post a top-level comment |
|
|
@@ -393,6 +431,7 @@ redbook post --title "测试" --body "..." --images img.png --private
|
|
|
393
431
|
|--------|-------------|---------|
|
|
394
432
|
| `--cookie-source <browser>` | Browser to read cookies from (chrome, safari, firefox) | `chrome` |
|
|
395
433
|
| `--chrome-profile <name>` | Chrome profile directory name (e.g., "Profile 1"). Auto-detected if omitted. | auto |
|
|
434
|
+
| `--cookie-string <cookies>` | Manual cookie string: `"a1=VALUE; web_session=VALUE"` (from Chrome DevTools) | none |
|
|
396
435
|
| `--json` | Output as JSON | `false` |
|
|
397
436
|
|
|
398
437
|
### Search Options
|
|
@@ -456,13 +495,20 @@ Publishing **frequently triggers captcha** (type=124). Image upload works, but t
|
|
|
456
495
|
| Problem | Solution |
|
|
457
496
|
|---------|----------|
|
|
458
497
|
| `No 'a1' cookie found` | Log into xiaohongshu.com in Chrome, then retry |
|
|
498
|
+
| Windows `-101` error | Chrome 127+ App-Bound Encryption. **Close Chrome first**, then re-run (CLI auto-launches Chrome headless to read cookies). If it still fails, use `--cookie-string` |
|
|
499
|
+
| Windows `--cookie-string` | Press F12 in Chrome → Application → Cookies → xiaohongshu.com. Copy `a1` and `web_session` values: `redbook whoami --cookie-string "a1=VALUE; web_session=VALUE"` |
|
|
459
500
|
| macOS Keychain prompt | Enter your password and click "Always Allow" — the CLI needs to decrypt Chrome's cookies |
|
|
460
|
-
| Multiple Chrome profiles | The CLI auto-scans all profiles. To pick one: `--chrome-profile "Profile 1"` |
|
|
501
|
+
| Multiple Chrome profiles | The CLI auto-scans all profiles (macOS / Windows / Linux). To pick one: `--chrome-profile "Profile 1"` |
|
|
461
502
|
| Using Brave/Arc/other | Try `--cookie-source safari`, or log into xiaohongshu.com in Chrome |
|
|
462
503
|
|
|
463
504
|
## How It Works
|
|
464
505
|
|
|
465
|
-
`redbook` reads your XHS session cookies from Chrome
|
|
506
|
+
`redbook` reads your XHS session cookies from Chrome and signs API requests using a TypeScript port of the XHS signing algorithm.
|
|
507
|
+
|
|
508
|
+
**Three-tier cookie extraction:**
|
|
509
|
+
1. **sweet-cookie** (fast path) — reads Chrome's SQLite cookie database directly. Works instantly on macOS
|
|
510
|
+
2. **CDP fallback** (auto on Windows) — launches Chrome headless and reads cookies via DevTools Protocol, bypassing Chrome 127+ App-Bound Encryption
|
|
511
|
+
3. **`--cookie-string`** (manual fallback) — paste cookie values from Chrome DevTools. Works on any platform
|
|
466
512
|
|
|
467
513
|
**Two signing systems:**
|
|
468
514
|
- **Main API** (`edith.xiaohongshu.com`) — for reading: search, feed, notes, comments, user profiles. Uses x-s signature with 144-byte payload (v4.3.1).
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ description: Search, read, analyze, and automate Xiaohongshu (小红书) content
|
|
|
3
3
|
allowed-tools: Bash, Read, Write, Glob, Grep
|
|
4
4
|
# OpenClaw / ClawHub metadata (clawhub install redbook)
|
|
5
5
|
name: redbook
|
|
6
|
-
version: 0.
|
|
6
|
+
version: 0.5.0
|
|
7
7
|
metadata:
|
|
8
8
|
openclaw:
|
|
9
9
|
requires:
|
|
@@ -53,6 +53,9 @@ Use the `redbook` CLI to search notes, read content, analyze creators, automate
|
|
|
53
53
|
| Post a comment | `redbook comment <url> --content "text"` |
|
|
54
54
|
| Reply to comment | `redbook reply <url> --comment-id <id> --content "text"` |
|
|
55
55
|
| Batch reply (preview) | `redbook batch-reply <url> --strategy questions --dry-run` |
|
|
56
|
+
| List favorites | `redbook favorites --json` or `redbook favorites <userId> --json` |
|
|
57
|
+
| Collect a note | `redbook collect <url>` |
|
|
58
|
+
| Remove from collection | `redbook uncollect <url>` |
|
|
56
59
|
| Render markdown to cards | `redbook render content.md --style xiaohongshu` |
|
|
57
60
|
| Check connection | `redbook whoami` |
|
|
58
61
|
|
|
@@ -598,8 +601,8 @@ XHS enforces aggressive anti-spam (风控) that detects automated behavior throu
|
|
|
598
601
|
## API vs Browser Limitations
|
|
599
602
|
|
|
600
603
|
The following operations work reliably via API:
|
|
601
|
-
- **Reading**: search, notes, comments, user profiles, feed
|
|
602
|
-
- **Writing**: top-level comments, comment replies
|
|
604
|
+
- **Reading**: search, notes, comments, user profiles, feed, favorites
|
|
605
|
+
- **Writing**: top-level comments, comment replies, collect/uncollect notes
|
|
603
606
|
- **Analysis**: viral scoring, template extraction, batch reply planning
|
|
604
607
|
|
|
605
608
|
The following operations are unreliable via API (frequently trigger captcha):
|
|
@@ -608,7 +611,7 @@ The following operations are unreliable via API (frequently trigger captcha):
|
|
|
608
611
|
|
|
609
612
|
The following operations require browser automation (not supported by this CLI):
|
|
610
613
|
- Captcha solving, real-time notifications
|
|
611
|
-
- Like/
|
|
614
|
+
- Like/follow (heavy anti-automation enforcement)
|
|
612
615
|
- DM/private messaging
|
|
613
616
|
- Cover image generation (use external tools like Gemini/DALL-E)
|
|
614
617
|
|
|
@@ -685,6 +688,37 @@ Search for topic hashtags. Useful for finding trending topics to attach to posts
|
|
|
685
688
|
redbook topics "Claude Code" --json
|
|
686
689
|
```
|
|
687
690
|
|
|
691
|
+
### `redbook favorites [userId]`
|
|
692
|
+
|
|
693
|
+
List a user's collected (bookmarked) notes. Defaults to the current logged-in user when no userId is provided.
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
redbook favorites --json # Your own favorites
|
|
697
|
+
redbook favorites "5a1234567890abcdef" --json # Another user's favorites
|
|
698
|
+
redbook favorites --all --json # Fetch all pages
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Options:**
|
|
702
|
+
- `--all`: Fetch all pages of favorites (default: first page only)
|
|
703
|
+
|
|
704
|
+
**Note:** Other users' favorites are only visible if they haven't set their collection to private.
|
|
705
|
+
|
|
706
|
+
### `redbook collect <url>`
|
|
707
|
+
|
|
708
|
+
Collect (bookmark) a note to your favorites.
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
redbook collect "https://www.xiaohongshu.com/explore/abc123"
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### `redbook uncollect <url>`
|
|
715
|
+
|
|
716
|
+
Remove a note from your collection.
|
|
717
|
+
|
|
718
|
+
```bash
|
|
719
|
+
redbook uncollect "https://www.xiaohongshu.com/explore/abc123"
|
|
720
|
+
```
|
|
721
|
+
|
|
688
722
|
### `redbook analyze-viral <url>`
|
|
689
723
|
|
|
690
724
|
Analyze why a viral note works. Returns a deterministic viral score (0–100).
|
package/dist/cli.d.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* redbook feed --cookie-source chrome --json
|
|
13
13
|
* redbook post --title "..." --body "..." --images img1.jpg --cookie-source chrome
|
|
14
14
|
* redbook topics "keyword" --cookie-source chrome
|
|
15
|
+
* redbook favorites --cookie-source chrome --json
|
|
16
|
+
* redbook collect <url> --cookie-source chrome
|
|
17
|
+
* redbook uncollect <url> --cookie-source chrome
|
|
15
18
|
*/
|
|
16
19
|
export {};
|
|
17
20
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;GAgBG"}
|
package/dist/cli.js
CHANGED
|
@@ -12,13 +12,16 @@
|
|
|
12
12
|
* redbook feed --cookie-source chrome --json
|
|
13
13
|
* redbook post --title "..." --body "..." --images img1.jpg --cookie-source chrome
|
|
14
14
|
* redbook topics "keyword" --cookie-source chrome
|
|
15
|
+
* redbook favorites --cookie-source chrome --json
|
|
16
|
+
* redbook collect <url> --cookie-source chrome
|
|
17
|
+
* redbook uncollect <url> --cookie-source chrome
|
|
15
18
|
*/
|
|
16
19
|
import { Command } from "commander";
|
|
17
20
|
import kleur from "kleur";
|
|
18
21
|
import { readFileSync } from "node:fs";
|
|
19
22
|
import { fileURLToPath } from "node:url";
|
|
20
23
|
import { dirname, join } from "node:path";
|
|
21
|
-
import { extractCookies } from "./lib/cookies.js";
|
|
24
|
+
import { extractCookies, parseCookieString } from "./lib/cookies.js";
|
|
22
25
|
import { XhsClient, XhsApiError } from "./lib/client.js";
|
|
23
26
|
import { analyzeViral, formatViralAnalysis } from "./lib/analyze.js";
|
|
24
27
|
import { selectCandidates, executeReplies, DEFAULT_DELAY_MS, MIN_DELAY_MS as MIN_REPLY_DELAY, MAX_REPLIES_HARD_CAP, } from "./lib/reply-strategy.js";
|
|
@@ -34,12 +37,23 @@ program
|
|
|
34
37
|
function addCookieOption(cmd) {
|
|
35
38
|
return cmd
|
|
36
39
|
.option("--cookie-source <browser>", "Browser to read cookies from (chrome, safari, firefox)", "chrome")
|
|
37
|
-
.option("--chrome-profile <name>", 'Chrome profile directory name (e.g., "Profile 1")')
|
|
40
|
+
.option("--chrome-profile <name>", 'Chrome profile directory name (e.g., "Profile 1")')
|
|
41
|
+
.option("--cookie-string <cookies>", 'Manual cookie string: "a1=VALUE; web_session=VALUE" (from Chrome DevTools)');
|
|
38
42
|
}
|
|
39
43
|
function addJsonOption(cmd) {
|
|
40
44
|
return cmd.option("--json", "Output as JSON");
|
|
41
45
|
}
|
|
42
|
-
async function getClient(cookieSource, chromeProfile) {
|
|
46
|
+
async function getClient(cookieSource, chromeProfile, cookieString) {
|
|
47
|
+
if (cookieString) {
|
|
48
|
+
const cookies = parseCookieString(cookieString);
|
|
49
|
+
if (!cookies.a1 || !cookies.web_session) {
|
|
50
|
+
console.error(kleur.red("Cookie string must contain at least 'a1' and 'web_session'. " +
|
|
51
|
+
"Copy them from Chrome DevTools > Application > Cookies > xiaohongshu.com"));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
console.error(kleur.dim("Using manual cookie string."));
|
|
55
|
+
return new XhsClient(cookies);
|
|
56
|
+
}
|
|
43
57
|
const cookies = await extractCookies(cookieSource, chromeProfile);
|
|
44
58
|
return new XhsClient(cookies);
|
|
45
59
|
}
|
|
@@ -71,7 +85,7 @@ addCookieOption(whoamiCmd);
|
|
|
71
85
|
addJsonOption(whoamiCmd);
|
|
72
86
|
whoamiCmd.action(async (opts) => {
|
|
73
87
|
try {
|
|
74
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
88
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
75
89
|
const info = await client.getSelfInfo();
|
|
76
90
|
if (opts.json) {
|
|
77
91
|
output(info, true);
|
|
@@ -100,7 +114,7 @@ addCookieOption(searchCmd);
|
|
|
100
114
|
addJsonOption(searchCmd);
|
|
101
115
|
searchCmd.action(async (keyword, opts) => {
|
|
102
116
|
try {
|
|
103
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
117
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
104
118
|
const sortMap = {
|
|
105
119
|
general: "general",
|
|
106
120
|
popular: "popularity_descending",
|
|
@@ -141,7 +155,7 @@ addCookieOption(readCmd);
|
|
|
141
155
|
addJsonOption(readCmd);
|
|
142
156
|
readCmd.action(async (url, opts) => {
|
|
143
157
|
try {
|
|
144
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
158
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
145
159
|
const { noteId, xsecToken } = parseNoteUrl(url);
|
|
146
160
|
let result;
|
|
147
161
|
if (opts.api || xsecToken) {
|
|
@@ -186,7 +200,7 @@ addCookieOption(commentsCmd);
|
|
|
186
200
|
addJsonOption(commentsCmd);
|
|
187
201
|
commentsCmd.action(async (url, opts) => {
|
|
188
202
|
try {
|
|
189
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
203
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
190
204
|
const { noteId, xsecToken } = parseNoteUrl(url);
|
|
191
205
|
const allComments = [];
|
|
192
206
|
let cursor = "";
|
|
@@ -224,7 +238,7 @@ addCookieOption(userCmd);
|
|
|
224
238
|
addJsonOption(userCmd);
|
|
225
239
|
userCmd.action(async (userId, opts) => {
|
|
226
240
|
try {
|
|
227
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
241
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
228
242
|
const info = await client.getUserInfo(userId);
|
|
229
243
|
output(info, opts.json ?? false);
|
|
230
244
|
}
|
|
@@ -240,7 +254,7 @@ addCookieOption(userPostsCmd);
|
|
|
240
254
|
addJsonOption(userPostsCmd);
|
|
241
255
|
userPostsCmd.action(async (userId, opts) => {
|
|
242
256
|
try {
|
|
243
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
257
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
244
258
|
const result = await client.getUserNotes(userId);
|
|
245
259
|
if (opts.json) {
|
|
246
260
|
output(result, true);
|
|
@@ -268,7 +282,7 @@ addCookieOption(feedCmd);
|
|
|
268
282
|
addJsonOption(feedCmd);
|
|
269
283
|
feedCmd.action(async (opts) => {
|
|
270
284
|
try {
|
|
271
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
285
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
272
286
|
const result = await client.getHomeFeed(opts.category);
|
|
273
287
|
output(result, opts.json ?? false);
|
|
274
288
|
}
|
|
@@ -289,7 +303,7 @@ addCookieOption(postCmd);
|
|
|
289
303
|
addJsonOption(postCmd);
|
|
290
304
|
postCmd.action(async (opts) => {
|
|
291
305
|
try {
|
|
292
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
306
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
293
307
|
const imageFiles = opts.images ?? [];
|
|
294
308
|
if (imageFiles.length === 0) {
|
|
295
309
|
console.error(kleur.red("At least one image is required. Use --images <path>"));
|
|
@@ -345,7 +359,7 @@ addCookieOption(topicsCmd);
|
|
|
345
359
|
addJsonOption(topicsCmd);
|
|
346
360
|
topicsCmd.action(async (keyword, opts) => {
|
|
347
361
|
try {
|
|
348
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
362
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
349
363
|
const result = await client.searchTopics(keyword);
|
|
350
364
|
if (opts.json) {
|
|
351
365
|
output(result, true);
|
|
@@ -362,6 +376,94 @@ topicsCmd.action(async (keyword, opts) => {
|
|
|
362
376
|
handleError(err);
|
|
363
377
|
}
|
|
364
378
|
});
|
|
379
|
+
// ─── favorites ──────────────────────────────────────────────────────────────
|
|
380
|
+
const favoritesCmd = program
|
|
381
|
+
.command("favorites [userId]")
|
|
382
|
+
.description("List a user's collected/favorited notes (defaults to current user)")
|
|
383
|
+
.option("--all", "Fetch all pages");
|
|
384
|
+
addCookieOption(favoritesCmd);
|
|
385
|
+
addJsonOption(favoritesCmd);
|
|
386
|
+
favoritesCmd.action(async (userId, opts) => {
|
|
387
|
+
try {
|
|
388
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
389
|
+
if (!userId) {
|
|
390
|
+
const me = (await client.getSelfInfo());
|
|
391
|
+
userId = String(me.user_id ?? "");
|
|
392
|
+
if (!userId) {
|
|
393
|
+
console.error(kleur.red("Could not determine current user ID"));
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const allNotes = [];
|
|
398
|
+
let cursor = "";
|
|
399
|
+
let hasMore = true;
|
|
400
|
+
while (hasMore) {
|
|
401
|
+
const res = (await client.getUserCollectedNotes(userId, 30, cursor));
|
|
402
|
+
if (res.notes)
|
|
403
|
+
allNotes.push(...res.notes);
|
|
404
|
+
hasMore = opts.all ? (res.has_more ?? false) : false;
|
|
405
|
+
cursor = res.cursor ?? "";
|
|
406
|
+
}
|
|
407
|
+
if (opts.json) {
|
|
408
|
+
output(allNotes, true);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
for (const note of allNotes) {
|
|
412
|
+
const n = note;
|
|
413
|
+
const user = n.user;
|
|
414
|
+
console.log(`${kleur.bold(String(n.display_title ?? n.title ?? "(no title)"))} ${kleur.dim(String(n.note_id ?? ""))} ${kleur.dim(`@${user?.nickname ?? "?"}`)}`);
|
|
415
|
+
}
|
|
416
|
+
console.log(kleur.dim(`\n${allNotes.length} collected notes`));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
handleError(err);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
// ─── collect ────────────────────────────────────────────────────────────────
|
|
424
|
+
const collectCmd = program
|
|
425
|
+
.command("collect <url>")
|
|
426
|
+
.description("Collect (bookmark) a note");
|
|
427
|
+
addCookieOption(collectCmd);
|
|
428
|
+
addJsonOption(collectCmd);
|
|
429
|
+
collectCmd.action(async (url, opts) => {
|
|
430
|
+
try {
|
|
431
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
432
|
+
const { noteId } = parseNoteUrl(url);
|
|
433
|
+
const result = await client.collectNote(noteId);
|
|
434
|
+
if (opts.json) {
|
|
435
|
+
output(result, true);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
console.log(kleur.green("Note collected!"));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
handleError(err);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// ─── uncollect ──────────────────────────────────────────────────────────────
|
|
446
|
+
const uncollectCmd = program
|
|
447
|
+
.command("uncollect <url>")
|
|
448
|
+
.description("Remove a note from your collection");
|
|
449
|
+
addCookieOption(uncollectCmd);
|
|
450
|
+
addJsonOption(uncollectCmd);
|
|
451
|
+
uncollectCmd.action(async (url, opts) => {
|
|
452
|
+
try {
|
|
453
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
454
|
+
const { noteId } = parseNoteUrl(url);
|
|
455
|
+
const result = await client.uncollectNote(noteId);
|
|
456
|
+
if (opts.json) {
|
|
457
|
+
output(result, true);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
console.log(kleur.green("Note removed from collection!"));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
handleError(err);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
365
467
|
// ─── analyze-viral ──────────────────────────────────────────────────────────
|
|
366
468
|
const analyzeViralCmd = program
|
|
367
469
|
.command("analyze-viral <url>")
|
|
@@ -371,7 +473,7 @@ addCookieOption(analyzeViralCmd);
|
|
|
371
473
|
addJsonOption(analyzeViralCmd);
|
|
372
474
|
analyzeViralCmd.action(async (url, opts) => {
|
|
373
475
|
try {
|
|
374
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
476
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
375
477
|
const { noteId, xsecToken } = parseNoteUrl(url);
|
|
376
478
|
// 1. Fetch the note (same pattern as `read` — prefer HTML, API when xsec_token present)
|
|
377
479
|
let note;
|
|
@@ -478,7 +580,7 @@ viralTemplateCmd.action(async (urls, opts) => {
|
|
|
478
580
|
console.error(kleur.red("Maximum 3 URLs allowed"));
|
|
479
581
|
process.exit(1);
|
|
480
582
|
}
|
|
481
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
583
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
482
584
|
const commentPages = Math.min(parseInt(opts.commentPages) || 3, 10);
|
|
483
585
|
const analyses = [];
|
|
484
586
|
for (const url of urls) {
|
|
@@ -584,7 +686,7 @@ addCookieOption(commentCmd);
|
|
|
584
686
|
addJsonOption(commentCmd);
|
|
585
687
|
commentCmd.action(async (url, opts) => {
|
|
586
688
|
try {
|
|
587
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
689
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
588
690
|
const { noteId } = parseNoteUrl(url);
|
|
589
691
|
const result = await client.postComment(noteId, opts.content);
|
|
590
692
|
if (opts.json) {
|
|
@@ -609,7 +711,7 @@ addCookieOption(replyCmd);
|
|
|
609
711
|
addJsonOption(replyCmd);
|
|
610
712
|
replyCmd.action(async (url, opts) => {
|
|
611
713
|
try {
|
|
612
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
714
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
613
715
|
const { noteId } = parseNoteUrl(url);
|
|
614
716
|
const result = await client.replyComment(noteId, opts.commentId, opts.content);
|
|
615
717
|
if (opts.json) {
|
|
@@ -637,7 +739,7 @@ addCookieOption(batchReplyCmd);
|
|
|
637
739
|
addJsonOption(batchReplyCmd);
|
|
638
740
|
batchReplyCmd.action(async (url, opts) => {
|
|
639
741
|
try {
|
|
640
|
-
const client = await getClient(opts.cookieSource, opts.chromeProfile);
|
|
742
|
+
const client = await getClient(opts.cookieSource, opts.chromeProfile, opts.cookieString);
|
|
641
743
|
const { noteId, xsecToken } = parseNoteUrl(url);
|
|
642
744
|
const strategy = opts.strategy;
|
|
643
745
|
const max = Math.min(parseInt(opts.max) || 10, MAX_REPLIES_HARD_CAP);
|