@jackwener/opencli 1.0.5 → 1.1.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +36 -10
  3. package/README.zh-CN.md +3 -0
  4. package/SKILL.md +7 -2
  5. package/dist/bilibili.js +4 -2
  6. package/dist/cli-manifest.json +506 -6
  7. package/dist/cli.js +51 -1
  8. package/dist/clis/antigravity/serve.js +296 -47
  9. package/dist/clis/arxiv/paper.d.ts +1 -0
  10. package/dist/clis/arxiv/paper.js +21 -0
  11. package/dist/clis/arxiv/search.d.ts +1 -0
  12. package/dist/clis/arxiv/search.js +24 -0
  13. package/dist/clis/arxiv/utils.d.ts +18 -0
  14. package/dist/clis/arxiv/utils.js +49 -0
  15. package/dist/clis/boss/batchgreet.d.ts +1 -0
  16. package/dist/clis/boss/batchgreet.js +147 -0
  17. package/dist/clis/boss/exchange.d.ts +1 -0
  18. package/dist/clis/boss/exchange.js +111 -0
  19. package/dist/clis/boss/greet.d.ts +1 -0
  20. package/dist/clis/boss/greet.js +175 -0
  21. package/dist/clis/boss/invite.d.ts +1 -0
  22. package/dist/clis/boss/invite.js +158 -0
  23. package/dist/clis/boss/joblist.d.ts +1 -0
  24. package/dist/clis/boss/joblist.js +55 -0
  25. package/dist/clis/boss/mark.d.ts +1 -0
  26. package/dist/clis/boss/mark.js +141 -0
  27. package/dist/clis/boss/recommend.d.ts +1 -0
  28. package/dist/clis/boss/recommend.js +83 -0
  29. package/dist/clis/boss/stats.d.ts +1 -0
  30. package/dist/clis/boss/stats.js +116 -0
  31. package/dist/clis/sinafinance/news.d.ts +7 -0
  32. package/dist/clis/sinafinance/news.js +61 -0
  33. package/dist/clis/wikipedia/search.d.ts +1 -0
  34. package/dist/clis/wikipedia/search.js +30 -0
  35. package/dist/clis/wikipedia/summary.d.ts +1 -0
  36. package/dist/clis/wikipedia/summary.js +28 -0
  37. package/dist/clis/wikipedia/utils.d.ts +8 -0
  38. package/dist/clis/wikipedia/utils.js +18 -0
  39. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +64 -5
  40. package/dist/clis/xiaohongshu/creator-note-detail.js +258 -69
  41. package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
  42. package/dist/clis/xiaohongshu/creator-note-detail.test.js +211 -0
  43. package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
  44. package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
  45. package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
  46. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
  47. package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
  48. package/dist/clis/xiaohongshu/creator-notes.js +159 -71
  49. package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
  50. package/dist/clis/xiaohongshu/creator-notes.test.js +162 -0
  51. package/dist/external.d.ts +20 -0
  52. package/dist/external.js +159 -0
  53. package/docs/.vitepress/config.mts +1 -0
  54. package/docs/public/CNAME +1 -0
  55. package/package.json +1 -1
  56. package/src/bilibili.ts +4 -2
  57. package/src/browser/cdp.ts +3 -3
  58. package/src/cli.ts +56 -1
  59. package/src/clis/antigravity/README.md +3 -46
  60. package/src/clis/antigravity/serve.ts +323 -50
  61. package/src/clis/arxiv/paper.ts +21 -0
  62. package/src/clis/arxiv/search.ts +24 -0
  63. package/src/clis/arxiv/utils.ts +63 -0
  64. package/src/clis/boss/batchgreet.ts +167 -0
  65. package/src/clis/boss/exchange.ts +126 -0
  66. package/src/clis/boss/greet.ts +198 -0
  67. package/src/clis/boss/invite.ts +177 -0
  68. package/src/clis/boss/joblist.ts +63 -0
  69. package/src/clis/boss/mark.ts +155 -0
  70. package/src/clis/boss/recommend.ts +94 -0
  71. package/src/clis/boss/stats.ts +130 -0
  72. package/src/clis/chaoxing/README.md +2 -24
  73. package/src/clis/chatgpt/README.md +3 -42
  74. package/src/clis/chatwise/README.md +3 -36
  75. package/src/clis/codex/README.md +3 -32
  76. package/src/clis/cursor/README.md +3 -31
  77. package/src/clis/discord-app/README.md +2 -25
  78. package/src/clis/feishu/README.md +2 -17
  79. package/src/clis/neteasemusic/README.md +3 -29
  80. package/src/clis/notion/README.md +2 -26
  81. package/src/clis/sinafinance/news.ts +76 -0
  82. package/src/clis/wechat/README.md +2 -25
  83. package/src/clis/wikipedia/search.ts +32 -0
  84. package/src/clis/wikipedia/summary.ts +28 -0
  85. package/src/clis/wikipedia/utils.ts +20 -0
  86. package/src/clis/xiaohongshu/creator-note-detail.test.ts +223 -0
  87. package/src/clis/xiaohongshu/creator-note-detail.ts +340 -72
  88. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
  89. package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
  90. package/src/clis/xiaohongshu/creator-notes.test.ts +178 -0
  91. package/src/clis/xiaohongshu/creator-notes.ts +215 -75
  92. package/src/daemon.ts +3 -3
  93. package/src/external-clis.yaml +39 -0
  94. package/src/external.ts +182 -0
  95. package/CDP.md +0 -103
  96. package/CDP.zh-CN.md +0 -103
  97. package/CLI-ELECTRON.md +0 -125
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { summarizeCreatorNote } from './creator-notes-summary.js';
3
+ import type { CreatorNoteRow } from './creator-notes.js';
4
+ import type { CreatorNoteDetailRow } from './creator-note-detail.js';
5
+ import './creator-notes-summary.js';
6
+
7
+ describe('xiaohongshu creator-notes-summary', () => {
8
+ it('summarizes note list row and detail rows into one compact row', () => {
9
+ const note: CreatorNoteRow = {
10
+ id: '69ba940500000000200384db',
11
+ title: '一张图讲清 诡秘之主·耕种者途径',
12
+ date: '2026年03月18日 20:01',
13
+ views: 549,
14
+ likes: 19,
15
+ collects: 10,
16
+ comments: 7,
17
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
18
+ };
19
+
20
+ const rows: CreatorNoteDetailRow[] = [
21
+ { section: '笔记信息', metric: 'published_at', value: '2026-03-18 20:01', extra: '' },
22
+ { section: '基础数据', metric: '观看数', value: '549', extra: '' },
23
+ { section: '互动数据', metric: '点赞数', value: '19', extra: '' },
24
+ { section: '互动数据', metric: '收藏数', value: '10', extra: '' },
25
+ { section: '互动数据', metric: '评论数', value: '7', extra: '' },
26
+ { section: '互动数据', metric: '分享数', value: '6', extra: '' },
27
+ { section: '基础数据', metric: '平均观看时长', value: '51.5秒', extra: '' },
28
+ { section: '基础数据', metric: '涨粉数', value: '3', extra: '' },
29
+ { section: '观看来源', metric: '首页推荐', value: '89.9%', extra: '' },
30
+ { section: '观看来源', metric: '搜索', value: '0.3%', extra: '' },
31
+ { section: '观众画像', metric: '兴趣/二次元', value: '13%', extra: '' },
32
+ { section: '观众画像', metric: '兴趣/游戏', value: '11%', extra: '' },
33
+ ];
34
+
35
+ expect(summarizeCreatorNote(note, rows, 1)).toEqual({
36
+ rank: 1,
37
+ id: '69ba940500000000200384db',
38
+ title: '一张图讲清 诡秘之主·耕种者途径',
39
+ published_at: '2026-03-18 20:01',
40
+ views: '549',
41
+ likes: '19',
42
+ collects: '10',
43
+ comments: '7',
44
+ shares: '6',
45
+ avg_view_time: '51.5秒',
46
+ rise_fans: '3',
47
+ top_source: '首页推荐',
48
+ top_source_pct: '89.9%',
49
+ top_interest: '二次元',
50
+ top_interest_pct: '13%',
51
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Xiaohongshu Creator Notes Summary — batch summary for recent notes.
3
+ *
4
+ * Combines creator-notes and creator-note-detail into a single command that
5
+ * returns one summary row per note, suitable for quick review or downstream JSON use.
6
+ */
7
+
8
+ import { cli, Strategy } from '../../registry.js';
9
+ import { fetchCreatorNotes, type CreatorNoteRow } from './creator-notes.js';
10
+ import { fetchCreatorNoteDetailRows, type CreatorNoteDetailRow } from './creator-note-detail.js';
11
+
12
+ type CreatorNoteSummaryRow = {
13
+ rank: number;
14
+ id: string;
15
+ title: string;
16
+ published_at: string;
17
+ views: string;
18
+ likes: string;
19
+ collects: string;
20
+ comments: string;
21
+ shares: string;
22
+ avg_view_time: string;
23
+ rise_fans: string;
24
+ top_source: string;
25
+ top_source_pct: string;
26
+ top_interest: string;
27
+ top_interest_pct: string;
28
+ url: string;
29
+ };
30
+
31
+ function findDetailValue(rows: CreatorNoteDetailRow[], metric: string): string {
32
+ return rows.find((row) => row.metric === metric)?.value ?? '';
33
+ }
34
+
35
+ function findTopBySectionPrefix(rows: CreatorNoteDetailRow[], section: string, prefix: string): { label: string; value: string } {
36
+ const matches = rows.filter((row) => row.section === section && row.metric.startsWith(prefix) && row.value);
37
+ if (matches.length === 0) return { label: '', value: '' };
38
+ const sorted = [...matches].sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
39
+ const top = sorted[0];
40
+ return {
41
+ label: top.metric.slice(prefix.length),
42
+ value: top.value,
43
+ };
44
+ }
45
+
46
+ export function summarizeCreatorNote(note: CreatorNoteRow, rows: CreatorNoteDetailRow[], rank: number): CreatorNoteSummaryRow {
47
+ const topSource = findTopBySectionPrefix(rows, '观看来源', '');
48
+ const topInterest = findTopBySectionPrefix(rows, '观众画像', '兴趣/');
49
+
50
+ return {
51
+ rank,
52
+ id: note.id,
53
+ title: note.title,
54
+ published_at: findDetailValue(rows, 'published_at') || note.date,
55
+ views: findDetailValue(rows, '观看数') || String(note.views),
56
+ likes: findDetailValue(rows, '点赞数') || String(note.likes),
57
+ collects: findDetailValue(rows, '收藏数') || String(note.collects),
58
+ comments: findDetailValue(rows, '评论数') || String(note.comments),
59
+ shares: findDetailValue(rows, '分享数'),
60
+ avg_view_time: findDetailValue(rows, '平均观看时长'),
61
+ rise_fans: findDetailValue(rows, '涨粉数'),
62
+ top_source: topSource.label,
63
+ top_source_pct: topSource.value,
64
+ top_interest: topInterest.label,
65
+ top_interest_pct: topInterest.value,
66
+ url: note.url,
67
+ };
68
+ }
69
+
70
+ cli({
71
+ site: 'xiaohongshu',
72
+ name: 'creator-notes-summary',
73
+ description: '小红书最近笔记批量摘要 (列表 + 单篇关键数据汇总)',
74
+ domain: 'creator.xiaohongshu.com',
75
+ strategy: Strategy.COOKIE,
76
+ browser: true,
77
+ args: [
78
+ { name: 'limit', type: 'int', default: 3, help: 'Number of recent notes to summarize' },
79
+ ],
80
+ columns: ['rank', 'id', 'title', 'views', 'likes', 'collects', 'comments', 'shares', 'avg_view_time', 'rise_fans', 'top_source', 'top_interest', 'url'],
81
+ timeoutSeconds: 180,
82
+ func: async (page, kwargs) => {
83
+ const limit = kwargs.limit || 3;
84
+ const notes = await fetchCreatorNotes(page, limit);
85
+
86
+ if (!notes.length) {
87
+ throw new Error('No notes found. Are you logged into creator.xiaohongshu.com?');
88
+ }
89
+
90
+ const results: CreatorNoteSummaryRow[] = [];
91
+ for (const [index, note] of notes.entries()) {
92
+ if (!note.id) {
93
+ results.push({
94
+ rank: index + 1,
95
+ id: note.id,
96
+ title: note.title,
97
+ published_at: note.date,
98
+ views: String(note.views),
99
+ likes: String(note.likes),
100
+ collects: String(note.collects),
101
+ comments: String(note.comments),
102
+ shares: '',
103
+ avg_view_time: '',
104
+ rise_fans: '',
105
+ top_source: '',
106
+ top_source_pct: '',
107
+ top_interest: '',
108
+ top_interest_pct: '',
109
+ url: note.url,
110
+ });
111
+ continue;
112
+ }
113
+
114
+ const detailRows = await fetchCreatorNoteDetailRows(page, note.id);
115
+ results.push(summarizeCreatorNote(note, detailRows, index + 1));
116
+ }
117
+
118
+ return results;
119
+ },
120
+ });
@@ -0,0 +1,178 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { IPage } from '../../types.js';
3
+ import { getRegistry } from '../../registry.js';
4
+ import { parseCreatorNoteIdsFromHtml, parseCreatorNotesText } from './creator-notes.js';
5
+ import './creator-notes.js';
6
+
7
+ function createPageMock(evaluateResult: any, interceptedRequests: any[] = []): IPage {
8
+ const evaluate = Array.isArray(evaluateResult)
9
+ ? vi.fn()
10
+ .mockResolvedValueOnce(evaluateResult[0])
11
+ .mockResolvedValue(evaluateResult[evaluateResult.length - 1])
12
+ : vi.fn().mockResolvedValue(evaluateResult);
13
+
14
+ const getInterceptedRequests = Array.isArray(interceptedRequests)
15
+ ? vi.fn().mockResolvedValue(interceptedRequests)
16
+ : vi.fn().mockResolvedValue([]);
17
+
18
+ return {
19
+ goto: vi.fn().mockResolvedValue(undefined),
20
+ evaluate,
21
+ snapshot: vi.fn().mockResolvedValue(undefined),
22
+ click: vi.fn().mockResolvedValue(undefined),
23
+ typeText: vi.fn().mockResolvedValue(undefined),
24
+ pressKey: vi.fn().mockResolvedValue(undefined),
25
+ wait: vi.fn().mockResolvedValue(undefined),
26
+ tabs: vi.fn().mockResolvedValue([]),
27
+ closeTab: vi.fn().mockResolvedValue(undefined),
28
+ newTab: vi.fn().mockResolvedValue(undefined),
29
+ selectTab: vi.fn().mockResolvedValue(undefined),
30
+ networkRequests: vi.fn().mockResolvedValue([]),
31
+ consoleMessages: vi.fn().mockResolvedValue([]),
32
+ scroll: vi.fn().mockResolvedValue(undefined),
33
+ autoScroll: vi.fn().mockResolvedValue(undefined),
34
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
35
+ getInterceptedRequests,
36
+ getCookies: vi.fn().mockResolvedValue([]),
37
+ screenshot: vi.fn().mockResolvedValue(''),
38
+ };
39
+ }
40
+
41
+ describe('xiaohongshu creator-notes', () => {
42
+ it('parses creator note text blocks into rows', () => {
43
+ const bodyText = `笔记管理
44
+ 全部笔记(366)
45
+ 已发布
46
+ 神雕侠侣战力金字塔
47
+ 发布于 2025年12月04日 19:45
48
+ 148208
49
+ 324
50
+ 2279
51
+ 465
52
+ 32
53
+ 权限设置
54
+ 取消置顶
55
+ 编辑
56
+ 删除
57
+ 仅自己可见
58
+ 终于等到了!!!
59
+ 发布于 2026年03月18日 12:39
60
+ 10
61
+ 0
62
+ 0
63
+ 0
64
+ 0
65
+ 权限设置`;
66
+
67
+ expect(parseCreatorNotesText(bodyText)).toEqual([
68
+ {
69
+ id: '',
70
+ title: '神雕侠侣战力金字塔',
71
+ date: '2025年12月04日 19:45',
72
+ views: 148208,
73
+ likes: 324,
74
+ collects: 2279,
75
+ comments: 465,
76
+ url: '',
77
+ },
78
+ {
79
+ id: '',
80
+ title: '终于等到了!!!',
81
+ date: '2026年03月18日 12:39',
82
+ views: 10,
83
+ likes: 0,
84
+ collects: 0,
85
+ comments: 0,
86
+ url: '',
87
+ },
88
+ ]);
89
+ });
90
+
91
+ it('reads body text and returns ranked rows', async () => {
92
+ const cmd = getRegistry().get('xiaohongshu/creator-notes');
93
+ expect(cmd?.func).toBeTypeOf('function');
94
+
95
+ const page = createPageMock([
96
+ undefined,
97
+ {
98
+ text: `示例笔记
99
+ 发布于 2026年03月19日 12:00
100
+ 10
101
+ 2
102
+ 3
103
+ 4
104
+ 5
105
+ 权限设置`,
106
+ html: '"noteId":"69ba940500000000200384db"',
107
+ },
108
+ ]);
109
+
110
+ const result = await cmd!.func!(page, { limit: 1 });
111
+
112
+ expect((page.evaluate as any).mock.calls.at(-1)?.[0]).toBe('() => ({ text: document.body.innerText, html: document.body.innerHTML })');
113
+ expect(result).toEqual([
114
+ {
115
+ rank: 1,
116
+ id: '69ba940500000000200384db',
117
+ title: '示例笔记',
118
+ date: '2026年03月19日 12:00',
119
+ views: 10,
120
+ likes: 2,
121
+ collects: 3,
122
+ comments: 4,
123
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
124
+ },
125
+ ]);
126
+ });
127
+
128
+ it('prefers the creator analyze API and preserves note ids', async () => {
129
+ const cmd = getRegistry().get('xiaohongshu/creator-notes');
130
+ expect(cmd?.func).toBeTypeOf('function');
131
+
132
+ const page = createPageMock(undefined, [{
133
+ data: {
134
+ note_infos: [
135
+ {
136
+ id: '69ba940500000000200384db',
137
+ title: '一张图讲清 诡秘之主·耕种者途径',
138
+ post_time: new Date('2026-03-18T20:01:00+08:00').getTime(),
139
+ read_count: 521,
140
+ like_count: 18,
141
+ fav_count: 10,
142
+ comment_count: 7,
143
+ },
144
+ ],
145
+ },
146
+ }]);
147
+
148
+ const result = await cmd!.func!(page, { limit: 1 });
149
+
150
+ expect((page.installInterceptor as any).mock.calls[0][0]).toContain('/api/galaxy/creator/datacenter/note/analyze/list');
151
+ expect(result).toEqual([
152
+ {
153
+ rank: 1,
154
+ id: '69ba940500000000200384db',
155
+ title: '一张图讲清 诡秘之主·耕种者途径',
156
+ date: '2026年03月18日 20:01',
157
+ views: 521,
158
+ likes: 18,
159
+ collects: 10,
160
+ comments: 7,
161
+ url: 'https://creator.xiaohongshu.com/statistics/note-detail?noteId=69ba940500000000200384db',
162
+ },
163
+ ]);
164
+ });
165
+
166
+ it('extracts note ids from creator note-manager html', () => {
167
+ const html = `
168
+ <div>&quot;noteId&quot;:&quot;69ba940500000000200384db&quot;</div>
169
+ <div>&quot;noteId&quot;:&quot;69ba2c98000000001a026e0f&quot;</div>
170
+ <div>&quot;noteId&quot;:&quot;69ba940500000000200384db&quot;</div>
171
+ `;
172
+
173
+ expect(parseCreatorNoteIdsFromHtml(html)).toEqual([
174
+ '69ba940500000000200384db',
175
+ '69ba2c98000000001a026e0f',
176
+ ]);
177
+ });
178
+ });
@@ -1,15 +1,224 @@
1
1
  /**
2
2
  * Xiaohongshu Creator Note List — per-note metrics from the creator backend.
3
3
  *
4
- * Navigates to the note manager page and extracts per-note data from
5
- * the rendered DOM. This approach bypasses the v2 API signature requirement.
6
- *
7
- * Returns: note title, publish date, views, likes, collects, comments.
4
+ * In CDP mode we capture the real creator analytics API response so the list
5
+ * includes stable note ids and detail-page URLs. If that capture is unavailable,
6
+ * we fall back to the older interceptor and DOM parsing paths.
8
7
  *
9
8
  * Requires: logged into creator.xiaohongshu.com in Chrome.
10
9
  */
11
10
 
12
11
  import { cli, Strategy } from '../../registry.js';
12
+ import type { IPage } from '../../types.js';
13
+
14
+ const DATE_LINE_RE = /^发布于 (\d{4}年\d{2}月\d{2}日 \d{2}:\d{2})$/;
15
+ const METRIC_LINE_RE = /^\d+$/;
16
+ const VISIBILITY_LINE_RE = /可见$/;
17
+ const NOTE_ANALYZE_API_PATH = '/api/galaxy/creator/datacenter/note/analyze/list';
18
+ const NOTE_DETAIL_PAGE_URL = 'https://creator.xiaohongshu.com/statistics/note-detail';
19
+
20
+ type CreatorNoteRow = {
21
+ id: string;
22
+ title: string;
23
+ date: string;
24
+ views: number;
25
+ likes: number;
26
+ collects: number;
27
+ comments: number;
28
+ url: string;
29
+ };
30
+
31
+ export type { CreatorNoteRow };
32
+
33
+ type CreatorAnalyzeApiResponse = {
34
+ error?: string;
35
+ data?: {
36
+ note_infos?: Array<{
37
+ id?: string;
38
+ title?: string;
39
+ post_time?: number;
40
+ read_count?: number;
41
+ like_count?: number;
42
+ fav_count?: number;
43
+ comment_count?: number;
44
+ }>;
45
+ total?: number;
46
+ };
47
+ };
48
+
49
+ const NOTE_ID_HTML_RE = /&quot;noteId&quot;:&quot;([0-9a-f]{24})&quot;/g;
50
+
51
+ function buildNoteDetailUrl(noteId?: string): string {
52
+ return noteId ? `${NOTE_DETAIL_PAGE_URL}?noteId=${encodeURIComponent(noteId)}` : '';
53
+ }
54
+
55
+ function formatPostTime(ts?: number): string {
56
+ if (!ts) return '';
57
+ // XHS API timestamps are Beijing time (UTC+8)
58
+ const date = new Date(ts + 8 * 3600_000);
59
+ const pad = (value: number) => String(value).padStart(2, '0');
60
+ return `${date.getUTCFullYear()}年${pad(date.getUTCMonth() + 1)}月${pad(date.getUTCDate())}日 ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
61
+ }
62
+
63
+ export function parseCreatorNotesText(bodyText: string): CreatorNoteRow[] {
64
+ const lines = bodyText
65
+ .split('\n')
66
+ .map((line) => line.trim())
67
+ .filter(Boolean);
68
+
69
+ const results: CreatorNoteRow[] = [];
70
+ const seen = new Set<string>();
71
+
72
+ for (let i = 0; i < lines.length; i++) {
73
+ const dateMatch = lines[i].match(DATE_LINE_RE);
74
+ if (!dateMatch) continue;
75
+
76
+ let titleIndex = i - 1;
77
+ while (titleIndex >= 0 && VISIBILITY_LINE_RE.test(lines[titleIndex])) titleIndex--;
78
+ if (titleIndex < 0) continue;
79
+
80
+ const title = lines[titleIndex];
81
+ const metrics: number[] = [];
82
+ let cursor = i + 1;
83
+
84
+ while (cursor < lines.length && METRIC_LINE_RE.test(lines[cursor]) && metrics.length < 5) {
85
+ metrics.push(parseInt(lines[cursor], 10));
86
+ cursor++;
87
+ }
88
+
89
+ if (metrics.length < 4) continue;
90
+
91
+ const key = `${title}@@${dateMatch[1]}`;
92
+ if (seen.has(key)) continue;
93
+ seen.add(key);
94
+
95
+ results.push({
96
+ id: '',
97
+ title,
98
+ date: dateMatch[1],
99
+ views: metrics[0] ?? 0,
100
+ likes: metrics[1] ?? 0,
101
+ collects: metrics[2] ?? 0,
102
+ comments: metrics[3] ?? 0,
103
+ url: '',
104
+ });
105
+
106
+ i = cursor - 1;
107
+ }
108
+
109
+ return results;
110
+ }
111
+
112
+ export function parseCreatorNoteIdsFromHtml(bodyHtml: string): string[] {
113
+ const ids: string[] = [];
114
+ const seen = new Set<string>();
115
+
116
+ for (const match of bodyHtml.matchAll(NOTE_ID_HTML_RE)) {
117
+ const id = match[1];
118
+ if (!id || seen.has(id)) continue;
119
+ seen.add(id);
120
+ ids.push(id);
121
+ }
122
+
123
+ return ids;
124
+ }
125
+
126
+ function mapAnalyzeItems(items: NonNullable<CreatorAnalyzeApiResponse['data']>['note_infos']): CreatorNoteRow[] {
127
+ return (items ?? []).map((item) => ({
128
+ id: item.id ?? '',
129
+ title: item.title ?? '',
130
+ date: formatPostTime(item.post_time),
131
+ views: item.read_count ?? 0,
132
+ likes: item.like_count ?? 0,
133
+ collects: item.fav_count ?? 0,
134
+ comments: item.comment_count ?? 0,
135
+ url: buildNoteDetailUrl(item.id),
136
+ }));
137
+ }
138
+
139
+ async function fetchCreatorNotesByApi(page: IPage, limit: number): Promise<CreatorNoteRow[]> {
140
+ const pageSize = Math.min(Math.max(limit, 10), 20);
141
+ const maxPages = Math.max(1, Math.ceil(limit / pageSize));
142
+ const notes: CreatorNoteRow[] = [];
143
+
144
+ await page.goto(`https://creator.xiaohongshu.com/statistics/data-analysis?type=0&page_size=${pageSize}&page_num=1`);
145
+ await page.wait(4);
146
+
147
+ for (let pageNum = 1; pageNum <= maxPages && notes.length < limit; pageNum++) {
148
+ const apiPath = `${NOTE_ANALYZE_API_PATH}?type=0&page_size=${pageSize}&page_num=${pageNum}`;
149
+ const fetched = await page.evaluate(`
150
+ async () => {
151
+ try {
152
+ const resp = await fetch(${JSON.stringify(apiPath)}, { credentials: 'include' });
153
+ if (!resp.ok) return { error: 'HTTP ' + resp.status };
154
+ return await resp.json();
155
+ } catch (e) {
156
+ return { error: e?.message ?? String(e) };
157
+ }
158
+ }
159
+ `) as CreatorAnalyzeApiResponse | undefined;
160
+
161
+ let items = fetched?.data?.note_infos ?? [];
162
+
163
+ if (!items.length) {
164
+ await page.installInterceptor(NOTE_ANALYZE_API_PATH);
165
+ await page.evaluate(`
166
+ async () => {
167
+ try {
168
+ await fetch(${JSON.stringify(apiPath)}, { credentials: 'include' });
169
+ } catch {}
170
+ return true;
171
+ }
172
+ `);
173
+ await page.wait(1);
174
+ const intercepted = await page.getInterceptedRequests();
175
+ const data = intercepted.find((entry: CreatorAnalyzeApiResponse) => Array.isArray(entry?.data?.note_infos)) as CreatorAnalyzeApiResponse | undefined;
176
+ items = data?.data?.note_infos ?? [];
177
+ }
178
+
179
+ if (!items.length) break;
180
+
181
+ notes.push(...mapAnalyzeItems(items));
182
+ if (items.length < pageSize) break;
183
+ }
184
+
185
+ return notes.slice(0, limit);
186
+ }
187
+
188
+ export async function fetchCreatorNotes(page: IPage, limit: number): Promise<CreatorNoteRow[]> {
189
+ let notes = await fetchCreatorNotesByApi(page, limit);
190
+
191
+ if (notes.length === 0) {
192
+ await page.goto('https://creator.xiaohongshu.com/new/note-manager');
193
+ await page.wait(4);
194
+
195
+ const maxPageDowns = Math.max(0, Math.ceil(limit / 10) + 1);
196
+ for (let i = 0; i <= maxPageDowns; i++) {
197
+ const body = await page.evaluate('() => ({ text: document.body.innerText, html: document.body.innerHTML })') as {
198
+ text?: string;
199
+ html?: string;
200
+ };
201
+ const bodyText = typeof body?.text === 'string' ? body.text : '';
202
+ const bodyHtml = typeof body?.html === 'string' ? body.html : '';
203
+ const parsedNotes = parseCreatorNotesText(bodyText);
204
+ const noteIds = parseCreatorNoteIdsFromHtml(bodyHtml);
205
+ notes = parsedNotes.map((note, index) => {
206
+ const id = noteIds[index] ?? '';
207
+ return {
208
+ ...note,
209
+ id,
210
+ url: buildNoteDetailUrl(id),
211
+ };
212
+ });
213
+ if (notes.length >= limit || i === maxPageDowns) break;
214
+
215
+ await page.pressKey('PageDown');
216
+ await page.wait(1);
217
+ }
218
+ }
219
+
220
+ return notes.slice(0, limit);
221
+ }
13
222
 
14
223
  cli({
15
224
  site: 'xiaohongshu',
@@ -24,76 +233,7 @@ cli({
24
233
  columns: ['rank', 'id', 'title', 'date', 'views', 'likes', 'collects', 'comments', 'url'],
25
234
  func: async (page, kwargs) => {
26
235
  const limit = kwargs.limit || 20;
27
-
28
- // Navigate to note manager
29
- await page.goto('https://creator.xiaohongshu.com/new/note-manager');
30
- await page.wait(4);
31
-
32
- // Scroll to load more notes if needed
33
- await page.autoScroll({ times: Math.ceil(limit / 10), delayMs: 1500 });
34
-
35
- // Extract note data from rendered DOM
36
- const notes = await page.evaluate(`
37
- (() => {
38
- const results = [];
39
- // Note cards in the manager page contain title, date, and metric numbers
40
- // Each note card has a consistent structure with the title, date line,
41
- // and a row of 4 numbers (views, likes, collects, comments)
42
- const cards = document.querySelectorAll('[class*="note-item"], [class*="noteItem"], [class*="card"]');
43
-
44
- if (cards.length === 0) {
45
- // Fallback: parse from any container with note-like content
46
- const allText = document.body.innerText;
47
- const notePattern = /(.+?)\\s+发布于\\s+(\\d{4}年\\d{2}月\\d{2}日\\s+\\d{2}:\\d{2})\\s*(\\d+)\\s*(\\d+)\\s*(\\d+)\\s*(\\d+)/g;
48
- let match;
49
- while ((match = notePattern.exec(allText)) !== null) {
50
- results.push({
51
- title: match[1].trim(),
52
- date: match[2],
53
- views: parseInt(match[3]) || 0,
54
- likes: parseInt(match[4]) || 0,
55
- collects: parseInt(match[5]) || 0,
56
- comments: parseInt(match[6]) || 0,
57
- });
58
- }
59
- return results;
60
- }
61
-
62
- cards.forEach(card => {
63
- const text = card.innerText || '';
64
- const linkEl = card.querySelector('a[href*="/publish/"], a[href*="/note/"], a[href*="/explore/"]');
65
- const href = linkEl?.getAttribute('href') || '';
66
- const idMatch = href.match(/\/(?:publish|explore|note)\/([a-zA-Z0-9]+)/);
67
- // Try to extract structured data
68
- const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
69
- if (lines.length < 2) return;
70
-
71
- const title = lines[0];
72
- const dateLine = lines.find(l => l.includes('发布于'));
73
- const dateMatch = dateLine?.match(/发布于\\s+(\\d{4}年\\d{2}月\\d{2}日\\s+\\d{2}:\\d{2})/);
74
-
75
- // Remove the publish timestamp before collecting note metrics.
76
- // Otherwise year/month/day/hour digits are picked up as views/likes/etc.
77
- const metricText = dateLine ? text.replace(dateLine, ' ') : text;
78
- const nums = metricText.match(/(?:^|\\s)(\\d+)(?:\\s|$)/g)?.map(n => parseInt(n.trim())) || [];
79
-
80
- if (title && !title.includes('全部笔记')) {
81
- results.push({
82
- id: idMatch ? idMatch[1] : '',
83
- title: title.replace(/\\s+/g, ' ').substring(0, 80),
84
- date: dateMatch ? dateMatch[1] : '',
85
- views: nums[0] || 0,
86
- likes: nums[1] || 0,
87
- collects: nums[2] || 0,
88
- comments: nums[3] || 0,
89
- url: href ? new URL(href, window.location.origin).toString() : '',
90
- });
91
- }
92
- });
93
-
94
- return results;
95
- })()
96
- `);
236
+ const notes = await fetchCreatorNotes(page, limit);
97
237
 
98
238
  if (!Array.isArray(notes) || notes.length === 0) {
99
239
  throw new Error('No notes found. Are you logged into creator.xiaohongshu.com?');
@@ -101,7 +241,7 @@ cli({
101
241
 
102
242
  return notes
103
243
  .slice(0, limit)
104
- .map((n: any, i: number) => ({
244
+ .map((n: CreatorNoteRow, i: number) => ({
105
245
  rank: i + 1,
106
246
  id: n.id,
107
247
  title: n.title,
package/src/daemon.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
15
- import { WebSocketServer, WebSocket } from 'ws';
15
+ import { WebSocketServer, WebSocket, type RawData } from 'ws';
16
16
 
17
17
  const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
18
18
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
@@ -138,11 +138,11 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
138
138
  const httpServer = createServer((req, res) => { handleRequest(req, res).catch(() => { res.writeHead(500); res.end(); }); });
139
139
  const wss = new WebSocketServer({ server: httpServer, path: '/ext' });
140
140
 
141
- wss.on('connection', (ws) => {
141
+ wss.on('connection', (ws: WebSocket) => {
142
142
  console.error('[daemon] Extension connected');
143
143
  extensionWs = ws;
144
144
 
145
- ws.on('message', (data) => {
145
+ ws.on('message', (data: RawData) => {
146
146
  try {
147
147
  const msg = JSON.parse(data.toString());
148
148