@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.
Files changed (80) hide show
  1. package/README.md +26 -0
  2. package/README.zh-CN.md +3 -0
  3. package/SKILL.md +7 -2
  4. package/dist/cli-manifest.json +506 -6
  5. package/dist/cli.js +51 -1
  6. package/dist/clis/antigravity/serve.js +296 -47
  7. package/dist/clis/arxiv/paper.d.ts +1 -0
  8. package/dist/clis/arxiv/paper.js +21 -0
  9. package/dist/clis/arxiv/search.d.ts +1 -0
  10. package/dist/clis/arxiv/search.js +24 -0
  11. package/dist/clis/arxiv/utils.d.ts +18 -0
  12. package/dist/clis/arxiv/utils.js +49 -0
  13. package/dist/clis/boss/batchgreet.d.ts +1 -0
  14. package/dist/clis/boss/batchgreet.js +147 -0
  15. package/dist/clis/boss/exchange.d.ts +1 -0
  16. package/dist/clis/boss/exchange.js +111 -0
  17. package/dist/clis/boss/greet.d.ts +1 -0
  18. package/dist/clis/boss/greet.js +175 -0
  19. package/dist/clis/boss/invite.d.ts +1 -0
  20. package/dist/clis/boss/invite.js +158 -0
  21. package/dist/clis/boss/joblist.d.ts +1 -0
  22. package/dist/clis/boss/joblist.js +55 -0
  23. package/dist/clis/boss/mark.d.ts +1 -0
  24. package/dist/clis/boss/mark.js +141 -0
  25. package/dist/clis/boss/recommend.d.ts +1 -0
  26. package/dist/clis/boss/recommend.js +83 -0
  27. package/dist/clis/boss/stats.d.ts +1 -0
  28. package/dist/clis/boss/stats.js +116 -0
  29. package/dist/clis/sinafinance/news.d.ts +7 -0
  30. package/dist/clis/sinafinance/news.js +61 -0
  31. package/dist/clis/wikipedia/search.d.ts +1 -0
  32. package/dist/clis/wikipedia/search.js +30 -0
  33. package/dist/clis/wikipedia/summary.d.ts +1 -0
  34. package/dist/clis/wikipedia/summary.js +28 -0
  35. package/dist/clis/wikipedia/utils.d.ts +8 -0
  36. package/dist/clis/wikipedia/utils.js +18 -0
  37. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +64 -5
  38. package/dist/clis/xiaohongshu/creator-note-detail.js +258 -69
  39. package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
  40. package/dist/clis/xiaohongshu/creator-note-detail.test.js +211 -0
  41. package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
  42. package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
  43. package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
  44. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
  45. package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
  46. package/dist/clis/xiaohongshu/creator-notes.js +159 -71
  47. package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
  48. package/dist/clis/xiaohongshu/creator-notes.test.js +162 -0
  49. package/dist/external.d.ts +20 -0
  50. package/dist/external.js +159 -0
  51. package/docs/.vitepress/config.mts +1 -1
  52. package/docs/public/CNAME +1 -0
  53. package/package.json +1 -1
  54. package/src/browser/cdp.ts +3 -3
  55. package/src/cli.ts +56 -1
  56. package/src/clis/antigravity/serve.ts +323 -50
  57. package/src/clis/arxiv/paper.ts +21 -0
  58. package/src/clis/arxiv/search.ts +24 -0
  59. package/src/clis/arxiv/utils.ts +63 -0
  60. package/src/clis/boss/batchgreet.ts +167 -0
  61. package/src/clis/boss/exchange.ts +126 -0
  62. package/src/clis/boss/greet.ts +198 -0
  63. package/src/clis/boss/invite.ts +177 -0
  64. package/src/clis/boss/joblist.ts +63 -0
  65. package/src/clis/boss/mark.ts +155 -0
  66. package/src/clis/boss/recommend.ts +94 -0
  67. package/src/clis/boss/stats.ts +130 -0
  68. package/src/clis/sinafinance/news.ts +76 -0
  69. package/src/clis/wikipedia/search.ts +32 -0
  70. package/src/clis/wikipedia/summary.ts +28 -0
  71. package/src/clis/wikipedia/utils.ts +20 -0
  72. package/src/clis/xiaohongshu/creator-note-detail.test.ts +223 -0
  73. package/src/clis/xiaohongshu/creator-note-detail.ts +340 -72
  74. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
  75. package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
  76. package/src/clis/xiaohongshu/creator-notes.test.ts +178 -0
  77. package/src/clis/xiaohongshu/creator-notes.ts +215 -75
  78. package/src/daemon.ts +3 -3
  79. package/src/external-clis.yaml +39 -0
  80. 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>&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
 
@@ -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"