Skip to content

Commit 8f17259

Browse files
authored
feat: xiaohongshu creator flows migration (#124)
Migrated xiaohongshu creator flows to v1.0.2+. - creator-notes with API + DOM fallback - creator-note-detail with audience/trend data - creator-notes-summary batch overview - Tests: 3 files, 9 tests Co-authored-by: ajia <491387123@qq.com>
1 parent 2876750 commit 8f17259

9 files changed

Lines changed: 1343 additions & 153 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dist/
55
.opencli/
66
.mcp.json
77
*.log
8+
.DS_Store
89

910
# VitePress
1011
docs/.vitepress/dist

src/browser/cdp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* - Shared DOM helper methods extracted to reduce duplication with Page (P1 #5)
99
*/
1010

11-
import { WebSocket } from 'ws';
11+
import { WebSocket, type RawData } from 'ws';
1212
import type { IPage } from '../types.js';
1313
import { wrapForEval } from './utils.js';
1414
import {
@@ -63,12 +63,12 @@ export class CDPBridge {
6363
resolve(new CDPPage(this));
6464
});
6565

66-
ws.on('error', (err) => {
66+
ws.on('error', (err: Error) => {
6767
clearTimeout(timeout);
6868
reject(err);
6969
});
7070

71-
ws.on('message', (data) => {
71+
ws.on('message', (data: RawData) => {
7272
try {
7373
const msg = JSON.parse(data.toString());
7474
// Handle command responses
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import type { IPage } from '../../types.js';
3+
import { getRegistry } from '../../registry.js';
4+
import { appendAudienceRows, appendTrendRows, parseCreatorNoteDetailText } from './creator-note-detail.js';
5+
import './creator-note-detail.js';
6+
7+
function createPageMock(evaluateResult: any): IPage {
8+
return {
9+
goto: vi.fn().mockResolvedValue(undefined),
10+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
11+
snapshot: vi.fn().mockResolvedValue(undefined),
12+
click: vi.fn().mockResolvedValue(undefined),
13+
typeText: vi.fn().mockResolvedValue(undefined),
14+
pressKey: vi.fn().mockResolvedValue(undefined),
15+
wait: vi.fn().mockResolvedValue(undefined),
16+
tabs: vi.fn().mockResolvedValue([]),
17+
closeTab: vi.fn().mockResolvedValue(undefined),
18+
newTab: vi.fn().mockResolvedValue(undefined),
19+
selectTab: vi.fn().mockResolvedValue(undefined),
20+
networkRequests: vi.fn().mockResolvedValue([]),
21+
consoleMessages: vi.fn().mockResolvedValue([]),
22+
scroll: vi.fn().mockResolvedValue(undefined),
23+
autoScroll: vi.fn().mockResolvedValue(undefined),
24+
installInterceptor: vi.fn().mockResolvedValue(undefined),
25+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
26+
getCookies: vi.fn().mockResolvedValue([]),
27+
screenshot: vi.fn().mockResolvedValue(''),
28+
};
29+
}
30+
31+
describe('xiaohongshu creator-note-detail', () => {
32+
it('parses note detail page text into info and metric rows', () => {
33+
const bodyText = `笔记数据详情
34+
一张图讲清 诡秘之主·耕种者途径
35+
#诡秘之主
36+
#耕种者序列
37+
2026-03-18 20:01
38+
切换笔记
39+
笔记诊断
40+
核心数据
41+
观看来源
42+
观众画像
43+
核心数据
44+
基础数据
45+
部分数据统计中,次日可查看
46+
导出数据
47+
曝光数
48+
1733
49+
粉丝占比 6.6%
50+
实时
51+
观看数
52+
544
53+
粉丝占比 7.2%
54+
封面点击率
55+
18.6%
56+
粉丝 19.1%
57+
平均观看时长
58+
51.5秒
59+
粉丝 55.8秒
60+
涨粉数
61+
3
62+
按小时
63+
按天
64+
互动数据
65+
数据实时更新
66+
导出数据
67+
点赞数
68+
19
69+
粉丝占比 60%
70+
评论数
71+
7
72+
粉丝占比 33.3%
73+
收藏数
74+
10
75+
粉丝占比 33.3%
76+
分享数
77+
6
78+
粉丝占比 0%`;
79+
80+
expect(parseCreatorNoteDetailText(bodyText, '69ba940500000000200384db')).toEqual([
81+
{ section: '笔记信息', metric: 'note_id', value: '69ba940500000000200384db', extra: '' },
82+
{ section: '笔记信息', metric: 'title', value: '一张图讲清 诡秘之主·耕种者途径', extra: '' },
83+
{ section: '笔记信息', metric: 'published_at', value: '2026-03-18 20:01', extra: '' },
84+
{ section: '基础数据', metric: '曝光数', value: '1733', extra: '粉丝占比 6.6%' },
85+
{ section: '基础数据', metric: '观看数', value: '544', extra: '粉丝占比 7.2%' },
86+
{ section: '基础数据', metric: '封面点击率', value: '18.6%', extra: '粉丝 19.1%' },
87+
{ section: '基础数据', metric: '平均观看时长', value: '51.5秒', extra: '粉丝 55.8秒' },
88+
{ section: '基础数据', metric: '涨粉数', value: '3', extra: '' },
89+
{ section: '互动数据', metric: '点赞数', value: '19', extra: '粉丝占比 60%' },
90+
{ section: '互动数据', metric: '评论数', value: '7', extra: '粉丝占比 33.3%' },
91+
{ section: '互动数据', metric: '收藏数', value: '10', extra: '粉丝占比 33.3%' },
92+
{ section: '互动数据', metric: '分享数', value: '6', extra: '粉丝占比 0%' },
93+
]);
94+
});
95+
96+
it('appends audience source and portrait rows from API payloads', () => {
97+
const rows = appendAudienceRows([], {
98+
audienceSource: {
99+
source: [
100+
{
101+
title: '首页推荐',
102+
value_with_double: 89.9,
103+
info: {
104+
imp_count: 1469,
105+
view_count: 276,
106+
interaction_count: 15,
107+
},
108+
},
109+
],
110+
},
111+
audienceSourceDetail: {
112+
gender: [
113+
{ title: '男性', value: 82 },
114+
{ title: '女性', value: 18 },
115+
],
116+
age: [
117+
{ title: '25-34', value: 55 },
118+
],
119+
city: [
120+
{ title: '上海', value: 8 },
121+
],
122+
interest: [
123+
{ title: '二次元', value: 13 },
124+
],
125+
},
126+
});
127+
128+
expect(rows).toEqual([
129+
{ section: '观看来源', metric: '首页推荐', value: '89.9%', extra: '曝光 1469 · 观看 276 · 互动 15' },
130+
{ section: '观众画像', metric: '性别/男性', value: '82%', extra: '' },
131+
{ section: '观众画像', metric: '性别/女性', value: '18%', extra: '' },
132+
{ section: '观众画像', metric: '年龄/25-34', value: '55%', extra: '' },
133+
{ section: '观众画像', metric: '城市/上海', value: '8%', extra: '' },
134+
{ section: '观众画像', metric: '兴趣/二次元', value: '13%', extra: '' },
135+
]);
136+
});
137+
138+
it('appends trend summary rows from hour/day series payloads', () => {
139+
const rows = appendTrendRows([], {
140+
audienceTrend: {
141+
no_data: true,
142+
no_data_tip_msg: '数据统计中,请稍后查看',
143+
},
144+
noteBase: {
145+
hour: {
146+
view_list: [
147+
{ date: new Date('2026-03-18T21:00:00+08:00').getTime(), count: 54 },
148+
{ date: new Date('2026-03-18T22:00:00+08:00').getTime(), count: 51 },
149+
],
150+
like_list: [
151+
{ date: new Date('2026-03-18T21:00:00+08:00').getTime(), count: 2 },
152+
],
153+
},
154+
day: {
155+
view_list: [
156+
{ date: new Date('2026-03-18T00:00:00+08:00').getTime(), count: 307 },
157+
],
158+
},
159+
},
160+
});
161+
162+
expect(rows).toEqual([
163+
{ section: '趋势说明', metric: '观众趋势', value: '暂不可用', extra: '数据统计中,请稍后查看' },
164+
{ section: '趋势数据', metric: '按小时/观看数', value: '2 points', extra: '03-18 21:00=54 | 03-18 22:00=51' },
165+
{ section: '趋势数据', metric: '按小时/点赞数', value: '1 points', extra: '03-18 21:00=2' },
166+
{ section: '趋势数据', metric: '按天/观看数', value: '1 points', extra: '2026-03-18=307' },
167+
]);
168+
});
169+
170+
it('navigates to the note detail page and returns parsed rows', async () => {
171+
const cmd = getRegistry().get('xiaohongshu/creator-note-detail');
172+
expect(cmd?.func).toBeTypeOf('function');
173+
174+
const page = createPageMock(`笔记数据详情
175+
示例笔记
176+
2026-03-19 12:00
177+
曝光数
178+
100
179+
粉丝占比 10%
180+
观看数
181+
50
182+
粉丝占比 20%
183+
封面点击率
184+
12%
185+
粉丝 11%
186+
平均观看时长
187+
30秒
188+
粉丝 31秒
189+
涨粉数
190+
2
191+
点赞数
192+
8
193+
粉丝占比 25%
194+
评论数
195+
1
196+
粉丝占比 0%
197+
收藏数
198+
3
199+
粉丝占比 50%
200+
分享数
201+
0
202+
粉丝占比 0%`);
203+
204+
const result = await cmd!.func!(page, { note_id: 'demo-note-id' });
205+
206+
expect((page.goto as any).mock.calls[0][0]).toBe('https://creator.xiaohongshu.com/statistics/note-detail?noteId=demo-note-id');
207+
expect((page.evaluate as any).mock.calls[0][0]).toBe('() => document.body.innerText');
208+
expect(result).toEqual([
209+
{ section: '笔记信息', metric: 'note_id', value: 'demo-note-id', extra: '' },
210+
{ section: '笔记信息', metric: 'title', value: '示例笔记', extra: '' },
211+
{ section: '笔记信息', metric: 'published_at', value: '2026-03-19 12:00', extra: '' },
212+
{ section: '基础数据', metric: '曝光数', value: '100', extra: '粉丝占比 10%' },
213+
{ section: '基础数据', metric: '观看数', value: '50', extra: '粉丝占比 20%' },
214+
{ section: '基础数据', metric: '封面点击率', value: '12%', extra: '粉丝 11%' },
215+
{ section: '基础数据', metric: '平均观看时长', value: '30秒', extra: '粉丝 31秒' },
216+
{ section: '基础数据', metric: '涨粉数', value: '2', extra: '' },
217+
{ section: '互动数据', metric: '点赞数', value: '8', extra: '粉丝占比 25%' },
218+
{ section: '互动数据', metric: '评论数', value: '1', extra: '粉丝占比 0%' },
219+
{ section: '互动数据', metric: '收藏数', value: '3', extra: '粉丝占比 50%' },
220+
{ section: '互动数据', metric: '分享数', value: '0', extra: '粉丝占比 0%' },
221+
]);
222+
});
223+
});

0 commit comments

Comments
 (0)