@jackwener/opencli 1.0.6 → 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.
- package/README.md +26 -0
- package/README.zh-CN.md +3 -0
- package/SKILL.md +7 -2
- package/dist/cli-manifest.json +506 -6
- package/dist/cli.js +51 -1
- package/dist/clis/antigravity/serve.js +296 -47
- package/dist/clis/arxiv/paper.d.ts +1 -0
- package/dist/clis/arxiv/paper.js +21 -0
- package/dist/clis/arxiv/search.d.ts +1 -0
- package/dist/clis/arxiv/search.js +24 -0
- package/dist/clis/arxiv/utils.d.ts +18 -0
- package/dist/clis/arxiv/utils.js +49 -0
- package/dist/clis/boss/batchgreet.d.ts +1 -0
- package/dist/clis/boss/batchgreet.js +147 -0
- package/dist/clis/boss/exchange.d.ts +1 -0
- package/dist/clis/boss/exchange.js +111 -0
- package/dist/clis/boss/greet.d.ts +1 -0
- package/dist/clis/boss/greet.js +175 -0
- package/dist/clis/boss/invite.d.ts +1 -0
- package/dist/clis/boss/invite.js +158 -0
- package/dist/clis/boss/joblist.d.ts +1 -0
- package/dist/clis/boss/joblist.js +55 -0
- package/dist/clis/boss/mark.d.ts +1 -0
- package/dist/clis/boss/mark.js +141 -0
- package/dist/clis/boss/recommend.d.ts +1 -0
- package/dist/clis/boss/recommend.js +83 -0
- package/dist/clis/boss/stats.d.ts +1 -0
- package/dist/clis/boss/stats.js +116 -0
- package/dist/clis/sinafinance/news.d.ts +7 -0
- package/dist/clis/sinafinance/news.js +61 -0
- package/dist/clis/wikipedia/search.d.ts +1 -0
- package/dist/clis/wikipedia/search.js +30 -0
- package/dist/clis/wikipedia/summary.d.ts +1 -0
- package/dist/clis/wikipedia/summary.js +28 -0
- package/dist/clis/wikipedia/utils.d.ts +8 -0
- package/dist/clis/wikipedia/utils.js +18 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +64 -5
- package/dist/clis/xiaohongshu/creator-note-detail.js +258 -69
- package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +211 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
- package/dist/clis/xiaohongshu/creator-notes.js +159 -71
- package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes.test.js +162 -0
- package/dist/external.d.ts +20 -0
- package/dist/external.js +159 -0
- package/docs/.vitepress/config.mts +1 -1
- package/docs/public/CNAME +1 -0
- package/package.json +1 -1
- package/src/browser/cdp.ts +3 -3
- package/src/cli.ts +56 -1
- package/src/clis/antigravity/serve.ts +323 -50
- package/src/clis/arxiv/paper.ts +21 -0
- package/src/clis/arxiv/search.ts +24 -0
- package/src/clis/arxiv/utils.ts +63 -0
- package/src/clis/boss/batchgreet.ts +167 -0
- package/src/clis/boss/exchange.ts +126 -0
- package/src/clis/boss/greet.ts +198 -0
- package/src/clis/boss/invite.ts +177 -0
- package/src/clis/boss/joblist.ts +63 -0
- package/src/clis/boss/mark.ts +155 -0
- package/src/clis/boss/recommend.ts +94 -0
- package/src/clis/boss/stats.ts +130 -0
- package/src/clis/sinafinance/news.ts +76 -0
- package/src/clis/wikipedia/search.ts +32 -0
- package/src/clis/wikipedia/summary.ts +28 -0
- package/src/clis/wikipedia/utils.ts +20 -0
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +223 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +340 -72
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
- package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
- package/src/clis/xiaohongshu/creator-notes.test.ts +178 -0
- package/src/clis/xiaohongshu/creator-notes.ts +215 -75
- package/src/daemon.ts +3 -3
- package/src/external-clis.yaml +39 -0
- package/src/external.ts +182 -0
|
@@ -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>"noteId":"69ba940500000000200384db"</div>
|
|
169
|
+
<div>"noteId":"69ba2c98000000001a026e0f"</div>
|
|
170
|
+
<div>"noteId":"69ba940500000000200384db"</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
|
-
*
|
|
5
|
-
*
|
|
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 = /"noteId":"([0-9a-f]{24})"/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:
|
|
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
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
- name: gh
|
|
2
|
+
binary: gh
|
|
3
|
+
description: "GitHub CLI — repos, PRs, issues, releases, gists"
|
|
4
|
+
homepage: "https://cli.github.com"
|
|
5
|
+
tags: [github, git, dev]
|
|
6
|
+
install:
|
|
7
|
+
mac: "brew install gh"
|
|
8
|
+
|
|
9
|
+
- name: obsidian
|
|
10
|
+
binary: obsidian
|
|
11
|
+
description: "Obsidian vault management — notes, search, tags, tasks, sync"
|
|
12
|
+
homepage: "https://obsidian.md/help/cli"
|
|
13
|
+
tags: [notes, knowledge, markdown]
|
|
14
|
+
install:
|
|
15
|
+
mac: "brew install --cask obsidian"
|
|
16
|
+
|
|
17
|
+
- name: readwise
|
|
18
|
+
binary: readwise
|
|
19
|
+
description: "Readwise & Reader CLI — highlights, annotations, reading list"
|
|
20
|
+
homepage: "https://github.com/readwiseio/readwise-cli"
|
|
21
|
+
tags: [reading, highlights]
|
|
22
|
+
install:
|
|
23
|
+
default: "npm install -g @readwiseio/readwise-cli"
|
|
24
|
+
|
|
25
|
+
- name: kubectl
|
|
26
|
+
binary: kubectl
|
|
27
|
+
description: "Kubernetes command-line tool"
|
|
28
|
+
homepage: "https://kubernetes.io/docs/reference/kubectl/"
|
|
29
|
+
tags: [kubernetes, k8s, devops]
|
|
30
|
+
install:
|
|
31
|
+
mac: "brew install kubectl"
|
|
32
|
+
|
|
33
|
+
- name: docker
|
|
34
|
+
binary: docker
|
|
35
|
+
description: "Docker command-line interface"
|
|
36
|
+
homepage: "https://docs.docker.com/engine/reference/commandline/cli/"
|
|
37
|
+
tags: [docker, containers, devops]
|
|
38
|
+
install:
|
|
39
|
+
mac: "brew install --cask docker"
|