@jackwener/opencli 1.7.21 → 1.8.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 +31 -148
- package/README.zh-CN.md +38 -211
- package/cli-manifest.json +6423 -4260
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/boss/utils.js +17 -1
- package/clis/boss/utils.test.js +34 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +226 -0
- package/clis/suno/generate.test.js +243 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +62 -0
- package/clis/suno/utils.js +540 -0
- package/clis/suno/utils.test.js +223 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +12 -5
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/search.js +6 -2
- package/clis/twitter/search.test.js +6 -0
- package/clis/twitter/shared.js +144 -0
- package/clis/twitter/shared.test.js +429 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/weibo/comments.js +3 -4
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/envelope.test.js +85 -0
- package/clis/weibo/favorites.js +4 -4
- package/clis/weibo/feed.js +3 -5
- package/clis/weibo/hot.js +3 -4
- package/clis/weibo/me.js +3 -5
- package/clis/weibo/post.js +3 -4
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/search.js +4 -3
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weibo/user.js +3 -4
- package/clis/weibo/utils.js +34 -5
- package/clis/weibo/utils.test.js +36 -0
- package/clis/weread/search-regression.test.js +18 -11
- package/clis/weread/search.js +15 -7
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +2 -1
- package/clis/xiaohongshu/creator-note-detail.test.js +11 -0
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +2 -1
- package/clis/xiaohongshu/creator-notes.test.js +12 -0
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +299 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +12 -0
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +15 -2
- package/clis/zhihu/collection.test.js +46 -0
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -9
- package/clis/zhihu/question.test.js +111 -9
- package/clis/zhihu/search.js +206 -43
- package/clis/zhihu/search.test.js +198 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/cli.js +1 -1
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +110 -0
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +20 -0
- package/dist/src/external.d.ts +6 -1
- package/dist/src/external.test.js +19 -0
- package/dist/src/main.js +14 -2
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
buildChatUrl,
|
|
5
|
+
buildExtractChatStateEvaluate,
|
|
6
|
+
buildExtractInboxEvaluate,
|
|
7
|
+
buildSendMessageEvaluate,
|
|
8
|
+
normalizeLimit,
|
|
9
|
+
normalizeRank,
|
|
10
|
+
} from './im.js';
|
|
11
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
12
|
+
import './messages.js';
|
|
13
|
+
import './reply.js';
|
|
14
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
15
|
+
|
|
16
|
+
async function runBrowserScript(html, script, { url = 'https://www.goofish.com/im', beforeEval } = {}) {
|
|
17
|
+
const dom = new JSDOM(html, { url, runScripts: 'outside-only' });
|
|
18
|
+
beforeEval?.(dom.window);
|
|
19
|
+
return dom.window.eval(script);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('xianyu im shared helpers', () => {
|
|
23
|
+
it('strictly validates limits and ranks before browser side effects', () => {
|
|
24
|
+
expect(normalizeLimit(undefined, 20, 100)).toBe(20);
|
|
25
|
+
expect(normalizeLimit('', 20, 100)).toBe(20);
|
|
26
|
+
expect(normalizeLimit('100', 20, 100)).toBe(100);
|
|
27
|
+
expect(() => normalizeLimit(0, 20, 100)).toThrow(ArgumentError);
|
|
28
|
+
expect(() => normalizeLimit(3.8, 20, 100)).toThrow(ArgumentError);
|
|
29
|
+
expect(() => normalizeLimit(999, 20, 100)).toThrow(ArgumentError);
|
|
30
|
+
expect(normalizeRank(undefined)).toBe(0);
|
|
31
|
+
expect(normalizeRank(1)).toBe(1);
|
|
32
|
+
expect(() => normalizeRank(0)).toThrow(ArgumentError);
|
|
33
|
+
expect(() => normalizeRank('1.5')).toThrow(ArgumentError);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('extracts recent inbox conversations from visible IM rows', async () => {
|
|
37
|
+
const result = await runBrowserScript(`
|
|
38
|
+
<main>
|
|
39
|
+
<a href="/im?itemId=10001&peerUserId=90001" class="conversation unread">
|
|
40
|
+
<div class="name">张三</div>
|
|
41
|
+
<div class="title">MacBook Pro 14</div>
|
|
42
|
+
<div class="money">¥5999</div>
|
|
43
|
+
<div class="message">还在吗?</div>
|
|
44
|
+
<span class="badge">2</span>
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://www.goofish.com/im?itemId=10002&peerUserId=90002" class="conversation">
|
|
47
|
+
<div class="name">李四</div>
|
|
48
|
+
<div class="title">iPhone 15</div>
|
|
49
|
+
<div class="message">明天能发货</div>
|
|
50
|
+
</a>
|
|
51
|
+
</main>
|
|
52
|
+
`, buildExtractInboxEvaluate(10));
|
|
53
|
+
|
|
54
|
+
expect(result.requiresAuth).toBe(false);
|
|
55
|
+
expect(result.items).toEqual([
|
|
56
|
+
{
|
|
57
|
+
peer_name: '张三',
|
|
58
|
+
peer_user_id: '90001',
|
|
59
|
+
item_id: '10001',
|
|
60
|
+
item_title: 'MacBook Pro 14',
|
|
61
|
+
price: '¥5999',
|
|
62
|
+
last_message: '还在吗?',
|
|
63
|
+
unread: true,
|
|
64
|
+
unread_count: 2,
|
|
65
|
+
url: 'https://www.goofish.com/im?itemId=10001&peerUserId=90001',
|
|
66
|
+
row_index: 0,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
peer_name: '李四',
|
|
70
|
+
peer_user_id: '90002',
|
|
71
|
+
item_id: '10002',
|
|
72
|
+
item_title: 'iPhone 15',
|
|
73
|
+
price: '',
|
|
74
|
+
last_message: '明天能发货',
|
|
75
|
+
unread: false,
|
|
76
|
+
unread_count: 0,
|
|
77
|
+
url: 'https://www.goofish.com/im?itemId=10002&peerUserId=90002',
|
|
78
|
+
row_index: 1,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('extracts inbox conversations from the real virtualized conversation row shape', async () => {
|
|
84
|
+
const result = await runBrowserScript(`
|
|
85
|
+
<main>
|
|
86
|
+
<div id="conv-list-scrollable">
|
|
87
|
+
<div class="rc-virtual-list">
|
|
88
|
+
<div class="rc-virtual-list-holder">
|
|
89
|
+
<div class="rc-virtual-list-holder-inner">
|
|
90
|
+
<div class="conversation-item--abc">
|
|
91
|
+
<div>
|
|
92
|
+
<div><span><sup title="3"><span>3</span></sup></span></div>
|
|
93
|
+
<div>
|
|
94
|
+
<div><div>通知消息</div></div>
|
|
95
|
+
<div>订单即将自动确认收货</div>
|
|
96
|
+
<div>3小时前</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="conversation-item--abc">
|
|
101
|
+
<div>
|
|
102
|
+
<div>
|
|
103
|
+
<div><div>隔壁猫小小</div></div>
|
|
104
|
+
<div>亲,喜欢可以拍下,有问题留言哦~会尽快回复</div>
|
|
105
|
+
<div>21小时前</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</main>
|
|
114
|
+
`, buildExtractInboxEvaluate(10));
|
|
115
|
+
|
|
116
|
+
expect(result.items).toEqual([
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
peer_name: '通知消息',
|
|
119
|
+
last_message: '订单即将自动确认收货',
|
|
120
|
+
unread: true,
|
|
121
|
+
unread_count: 3,
|
|
122
|
+
}),
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
peer_name: '隔壁猫小小',
|
|
125
|
+
last_message: '亲,喜欢可以拍下,有问题留言哦~会尽快回复',
|
|
126
|
+
unread: false,
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('extracts all visible messages for a specific chat', async () => {
|
|
132
|
+
const state = await runBrowserScript(`
|
|
133
|
+
<main>
|
|
134
|
+
<textarea></textarea>
|
|
135
|
+
<button>发送</button>
|
|
136
|
+
<div class="message-topbar"><span class="text1">张三</span><span class="text2">(90001)</span></div>
|
|
137
|
+
<a href="/item?id=10001"><span class="title">MacBook Pro 14</span><span class="money">¥5999</span></a>
|
|
138
|
+
<div id="message-list-scrollable">
|
|
139
|
+
<div class="bubble">你好</div>
|
|
140
|
+
<div class="message">还在吗?</div>
|
|
141
|
+
<div class="msg">在的</div>
|
|
142
|
+
</div>
|
|
143
|
+
</main>
|
|
144
|
+
`, buildExtractChatStateEvaluate());
|
|
145
|
+
|
|
146
|
+
expect(state.peer_name).toBe('张三');
|
|
147
|
+
expect(state.peer_masked_id).toBe('90001');
|
|
148
|
+
expect(state.item_title).toBe('MacBook Pro 14');
|
|
149
|
+
expect(state.messages).toEqual([
|
|
150
|
+
{ index: 1, text: '你好' },
|
|
151
|
+
{ index: 2, text: '还在吗?' },
|
|
152
|
+
{ index: 3, text: '在的' },
|
|
153
|
+
]);
|
|
154
|
+
expect(state.visible_messages).toEqual(['你好', '还在吗?', '在的']);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('extracts messages from the real Xianyu message-row shape', async () => {
|
|
158
|
+
const state = await runBrowserScript(`
|
|
159
|
+
<main>
|
|
160
|
+
<textarea></textarea>
|
|
161
|
+
<button>发 送</button>
|
|
162
|
+
<div id="message-list-scrollable">
|
|
163
|
+
<div class="message-list-reverse--x">
|
|
164
|
+
<div class="message-row--a">
|
|
165
|
+
<div class="ant-dropdown-trigger message-content--b">
|
|
166
|
+
<div class="message-text--c message-text-right--d"><span>我发出的消息</span></div>
|
|
167
|
+
<div class="read-status-text--e">已读</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="message-row--a">
|
|
171
|
+
<div class="ant-dropdown-trigger message-content--b">
|
|
172
|
+
<div class="message-text--c message-text-left--d"><span>对方回复</span></div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</main>
|
|
178
|
+
`, buildExtractChatStateEvaluate());
|
|
179
|
+
|
|
180
|
+
expect(state.can_send).toBe(true);
|
|
181
|
+
expect(state.messages).toEqual([
|
|
182
|
+
{ index: 1, text: '我发出的消息' },
|
|
183
|
+
{ index: 2, text: '对方回复' },
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
it('registers reply as an explicit write command', () => {
|
|
189
|
+
const command = getRegistry().get('xianyu/reply');
|
|
190
|
+
expect(command?.access).toBe('write');
|
|
191
|
+
expect(command?.columns).toEqual(['status', 'peer_name', 'item_title', 'price', 'location', 'message']);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('builds chat URLs and requires post-submit message evidence', async () => {
|
|
195
|
+
expect(buildChatUrl('10001', '90001')).toBe('https://www.goofish.com/im?itemId=10001&peerUserId=90001');
|
|
196
|
+
|
|
197
|
+
let clicked = false;
|
|
198
|
+
const result = await runBrowserScript(`
|
|
199
|
+
<textarea></textarea>
|
|
200
|
+
<button>发送</button>
|
|
201
|
+
<div id="message-list-scrollable"></div>
|
|
202
|
+
`, buildSendMessageEvaluate('你好'), {
|
|
203
|
+
beforeEval(window) {
|
|
204
|
+
window.document.querySelector('button').addEventListener('click', () => {
|
|
205
|
+
clicked = true;
|
|
206
|
+
const row = window.document.createElement('div');
|
|
207
|
+
row.className = 'message-row';
|
|
208
|
+
row.innerHTML = '<div class="message-text">你好</div>';
|
|
209
|
+
window.document.querySelector('#message-list-scrollable').append(row);
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(result).toEqual({ ok: true });
|
|
215
|
+
expect(clicked).toBe(true);
|
|
216
|
+
|
|
217
|
+
await expect(runBrowserScript('<textarea></textarea><button>发送</button>', buildSendMessageEvaluate('没证据'), {
|
|
218
|
+
beforeEval(window) {
|
|
219
|
+
window.setTimeout = (fn) => {
|
|
220
|
+
fn();
|
|
221
|
+
return 0;
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
}))
|
|
225
|
+
.resolves.toEqual({ ok: false, reason: 'send-postcondition-timeout' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('requires an explicit conversation target for messages and reply', async () => {
|
|
229
|
+
const messages = getRegistry().get('xianyu/messages');
|
|
230
|
+
const reply = getRegistry().get('xianyu/reply');
|
|
231
|
+
const page = {
|
|
232
|
+
goto: () => { throw new Error('should not navigate'); },
|
|
233
|
+
wait: () => {},
|
|
234
|
+
evaluate: () => ({}),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
await expect(messages.func(page, { limit: 1 })).rejects.toBeInstanceOf(ArgumentError);
|
|
238
|
+
await expect(reply.func(page, { text: 'hello' })).rejects.toBeInstanceOf(ArgumentError);
|
|
239
|
+
await expect(messages.func(page, { item_id: '10001', user_id: '90001', rank: 1 })).rejects.toBeInstanceOf(ArgumentError);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('treats malformed evaluate payloads as command failures', async () => {
|
|
243
|
+
const messages = getRegistry().get('xianyu/messages');
|
|
244
|
+
const page = {
|
|
245
|
+
goto: () => {},
|
|
246
|
+
wait: () => {},
|
|
247
|
+
evaluate: () => null,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
await expect(messages.func(page, { item_id: '10001', user_id: '90001', limit: 1 }))
|
|
251
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import {
|
|
4
|
+
buildClickInboxConversationEvaluate,
|
|
5
|
+
buildExtractInboxEvaluate,
|
|
6
|
+
buildInboxUrl,
|
|
7
|
+
buildReadCurrentConversationUrlEvaluate,
|
|
8
|
+
DEFAULT_INBOX_LIMIT,
|
|
9
|
+
MAX_INBOX_LIMIT,
|
|
10
|
+
normalizeLimit,
|
|
11
|
+
requireClickResult,
|
|
12
|
+
requireEvaluateObject,
|
|
13
|
+
} from './im.js';
|
|
14
|
+
|
|
15
|
+
cli({
|
|
16
|
+
site: 'xianyu',
|
|
17
|
+
name: 'inbox',
|
|
18
|
+
access: 'read',
|
|
19
|
+
description: '列出闲鱼最近私信会话',
|
|
20
|
+
domain: 'www.goofish.com',
|
|
21
|
+
strategy: Strategy.COOKIE,
|
|
22
|
+
navigateBefore: false,
|
|
23
|
+
browser: true,
|
|
24
|
+
args: [
|
|
25
|
+
{ name: 'limit', type: 'int', default: DEFAULT_INBOX_LIMIT, help: 'Number of conversations to return' },
|
|
26
|
+
{ name: 'unread-only', type: 'bool', default: false, help: 'Return only conversations with unread messages' },
|
|
27
|
+
{ name: 'resolve-ids', type: 'bool', default: false, help: 'Click each visible conversation to resolve item_id and peer_user_id from the chat URL' },
|
|
28
|
+
],
|
|
29
|
+
columns: ['rank', 'peer_name', 'peer_user_id', 'item_id', 'item_title', 'price', 'last_message', 'unread', 'unread_count', 'url'],
|
|
30
|
+
func: async (page, kwargs) => {
|
|
31
|
+
const limit = normalizeLimit(kwargs.limit, DEFAULT_INBOX_LIMIT, MAX_INBOX_LIMIT, 'inbox --limit');
|
|
32
|
+
const unreadOnly = Boolean(kwargs['unread-only']);
|
|
33
|
+
const resolveIds = Boolean(kwargs['resolve-ids']);
|
|
34
|
+
let currentUrl = '';
|
|
35
|
+
if (page.getCurrentUrl) {
|
|
36
|
+
try {
|
|
37
|
+
currentUrl = await page.getCurrentUrl();
|
|
38
|
+
} catch {
|
|
39
|
+
currentUrl = '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!/https:\/\/www\.goofish\.com\/im\b/.test(currentUrl)) {
|
|
43
|
+
await page.goto(buildInboxUrl());
|
|
44
|
+
}
|
|
45
|
+
await page.wait(4);
|
|
46
|
+
const payload = requireEvaluateObject(await page.evaluate(buildExtractInboxEvaluate(limit)), 'inbox');
|
|
47
|
+
if (payload?.requiresAuth) {
|
|
48
|
+
throw new AuthRequiredError('www.goofish.com', 'Xianyu inbox requires a logged-in browser session');
|
|
49
|
+
}
|
|
50
|
+
if (payload?.blocked) {
|
|
51
|
+
throw new AuthRequiredError('www.goofish.com', 'Xianyu inbox is blocked by verification or risk control');
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(payload.items)) {
|
|
54
|
+
throw new CommandExecutionError('Xianyu inbox returned malformed conversation list');
|
|
55
|
+
}
|
|
56
|
+
const items = payload.items;
|
|
57
|
+
if (!items.length) {
|
|
58
|
+
throw new EmptyResultError('xianyu inbox', 'No Xianyu inbox conversations were found');
|
|
59
|
+
}
|
|
60
|
+
let conversations = items.slice(0, limit);
|
|
61
|
+
if (unreadOnly) {
|
|
62
|
+
conversations = conversations.filter((item) => Boolean(item.unread));
|
|
63
|
+
}
|
|
64
|
+
if (resolveIds) {
|
|
65
|
+
for (const item of conversations) {
|
|
66
|
+
if (item.item_id && item.peer_user_id) continue;
|
|
67
|
+
const rowIndex = Number(item.row_index);
|
|
68
|
+
if (!Number.isInteger(rowIndex) || rowIndex < 0) continue;
|
|
69
|
+
requireClickResult(await page.evaluate(buildClickInboxConversationEvaluate(rowIndex)), 'inbox resolve-ids click');
|
|
70
|
+
await page.wait(1);
|
|
71
|
+
const current = requireEvaluateObject(await page.evaluate(buildReadCurrentConversationUrlEvaluate()), 'inbox current-url');
|
|
72
|
+
item.item_id = current?.item_id || item.item_id || '';
|
|
73
|
+
item.peer_user_id = current?.peer_user_id || item.peer_user_id || '';
|
|
74
|
+
item.url = current?.url || item.url || '';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return conversations.map((item, index) => ({
|
|
78
|
+
rank: index + 1,
|
|
79
|
+
peer_name: item.peer_name || '',
|
|
80
|
+
peer_user_id: item.peer_user_id || '',
|
|
81
|
+
item_id: item.item_id || '',
|
|
82
|
+
item_title: item.item_title || '',
|
|
83
|
+
price: item.price || '',
|
|
84
|
+
last_message: item.last_message || '',
|
|
85
|
+
unread: Boolean(item.unread),
|
|
86
|
+
unread_count: Number(item.unread_count || 0),
|
|
87
|
+
url: item.url || '',
|
|
88
|
+
}));
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const __test__ = {
|
|
93
|
+
buildInboxUrl,
|
|
94
|
+
buildExtractInboxEvaluate,
|
|
95
|
+
normalizeLimit,
|
|
96
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import {
|
|
4
|
+
buildChatUrl,
|
|
5
|
+
buildClickInboxConversationEvaluate,
|
|
6
|
+
buildExtractChatStateEvaluate,
|
|
7
|
+
DEFAULT_MESSAGE_LIMIT,
|
|
8
|
+
MAX_MESSAGE_LIMIT,
|
|
9
|
+
normalizeLimit,
|
|
10
|
+
normalizeRank,
|
|
11
|
+
requireClickResult,
|
|
12
|
+
requireEvaluateObject,
|
|
13
|
+
} from './im.js';
|
|
14
|
+
import { normalizeNumericId } from './utils.js';
|
|
15
|
+
|
|
16
|
+
cli({
|
|
17
|
+
site: 'xianyu',
|
|
18
|
+
name: 'messages',
|
|
19
|
+
access: 'read',
|
|
20
|
+
description: '读取指定闲鱼私信会话的最近聊天内容',
|
|
21
|
+
domain: 'www.goofish.com',
|
|
22
|
+
strategy: Strategy.COOKIE,
|
|
23
|
+
navigateBefore: false,
|
|
24
|
+
browser: true,
|
|
25
|
+
args: [
|
|
26
|
+
{ name: 'item_id', positional: true, help: '闲鱼商品 item_id' },
|
|
27
|
+
{ name: 'user_id', positional: true, help: '聊一聊对方的 user_id / peerUserId' },
|
|
28
|
+
{ name: 'limit', type: 'int', default: DEFAULT_MESSAGE_LIMIT, help: 'Number of visible messages to return' },
|
|
29
|
+
{ name: 'rank', type: 'int', default: 0, help: 'Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs' },
|
|
30
|
+
],
|
|
31
|
+
columns: ['index', 'peer_name', 'item_title', 'message', 'item_id', 'peer_user_id', 'url'],
|
|
32
|
+
func: async (page, kwargs) => {
|
|
33
|
+
const hasItemId = kwargs.item_id != null && kwargs.item_id !== '';
|
|
34
|
+
const hasUserId = kwargs.user_id != null && kwargs.user_id !== '';
|
|
35
|
+
const rank = normalizeRank(kwargs.rank);
|
|
36
|
+
if (rank > 0 && (hasItemId || hasUserId)) {
|
|
37
|
+
throw new ArgumentError('xianyu messages accepts either item_id/user_id or --rank, not both');
|
|
38
|
+
}
|
|
39
|
+
if (rank === 0 && hasItemId !== hasUserId) {
|
|
40
|
+
throw new ArgumentError('xianyu messages requires both item_id and user_id, or --rank from xianyu inbox');
|
|
41
|
+
}
|
|
42
|
+
if (rank === 0 && !hasItemId && !hasUserId) {
|
|
43
|
+
throw new ArgumentError('xianyu messages requires item_id/user_id or --rank from xianyu inbox');
|
|
44
|
+
}
|
|
45
|
+
const hasIds = hasItemId && hasUserId;
|
|
46
|
+
const itemId = hasIds ? normalizeNumericId(kwargs.item_id, 'item_id', '1038951278192') : '';
|
|
47
|
+
const userId = hasIds ? normalizeNumericId(kwargs.user_id, 'user_id', '3650092411') : '';
|
|
48
|
+
const limit = normalizeLimit(kwargs.limit, DEFAULT_MESSAGE_LIMIT, MAX_MESSAGE_LIMIT, 'messages --limit');
|
|
49
|
+
let url = '';
|
|
50
|
+
if (hasIds) {
|
|
51
|
+
url = buildChatUrl(itemId, userId);
|
|
52
|
+
await page.goto(url);
|
|
53
|
+
} else {
|
|
54
|
+
if (!page.getCurrentUrl || !/https:\/\/www\.goofish\.com\/im\b/.test(await page.getCurrentUrl())) {
|
|
55
|
+
await page.goto('https://www.goofish.com/im');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await page.wait(2);
|
|
59
|
+
if (rank > 0) {
|
|
60
|
+
requireClickResult(await page.evaluate(buildClickInboxConversationEvaluate(rank - 1)), 'messages rank click');
|
|
61
|
+
await page.wait(2);
|
|
62
|
+
}
|
|
63
|
+
const state = requireEvaluateObject(await page.evaluate(buildExtractChatStateEvaluate(limit)), 'messages');
|
|
64
|
+
if (state?.requiresAuth) {
|
|
65
|
+
throw new AuthRequiredError('www.goofish.com', 'Xianyu messages requires a logged-in browser session');
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(state.messages)) {
|
|
68
|
+
throw new CommandExecutionError('Xianyu messages returned malformed message list');
|
|
69
|
+
}
|
|
70
|
+
const messages = state.messages;
|
|
71
|
+
if (!messages.length) {
|
|
72
|
+
throw new EmptyResultError('xianyu messages', 'No visible messages were found in this Xianyu conversation');
|
|
73
|
+
}
|
|
74
|
+
return messages.slice(-limit).map((message, index) => ({
|
|
75
|
+
index: index + 1,
|
|
76
|
+
peer_name: state.peer_name || '',
|
|
77
|
+
item_title: state.item_title || '',
|
|
78
|
+
message: message.text || '',
|
|
79
|
+
item_id: itemId,
|
|
80
|
+
peer_user_id: userId,
|
|
81
|
+
url: url || '',
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const __test__ = {
|
|
87
|
+
buildChatUrl,
|
|
88
|
+
buildExtractChatStateEvaluate,
|
|
89
|
+
normalizeLimit,
|
|
90
|
+
normalizeRank,
|
|
91
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError, selectorError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import { buildChatUrl, buildClickInboxConversationEvaluate, buildExtractChatStateEvaluate, buildSendMessageEvaluate, normalizeRank, requireClickResult, requireEvaluateObject, requireText } from './im.js';
|
|
4
|
+
import { normalizeNumericId } from './utils.js';
|
|
5
|
+
|
|
6
|
+
cli({
|
|
7
|
+
site: 'xianyu',
|
|
8
|
+
name: 'reply',
|
|
9
|
+
access: 'write',
|
|
10
|
+
description: '回复指定闲鱼私信会话',
|
|
11
|
+
domain: 'www.goofish.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
browser: true,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'item_id', positional: true, help: '闲鱼商品 item_id' },
|
|
17
|
+
{ name: 'user_id', positional: true, help: '聊一聊对方的 user_id / peerUserId' },
|
|
18
|
+
{ name: 'text', required: true, help: 'Message text to send' },
|
|
19
|
+
{ name: 'rank', type: 'int', default: 0, help: 'Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['status', 'peer_name', 'item_title', 'price', 'location', 'message'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const hasItemId = kwargs.item_id != null && kwargs.item_id !== '';
|
|
24
|
+
const hasUserId = kwargs.user_id != null && kwargs.user_id !== '';
|
|
25
|
+
const rank = normalizeRank(kwargs.rank);
|
|
26
|
+
if (rank > 0 && (hasItemId || hasUserId)) {
|
|
27
|
+
throw new ArgumentError('xianyu reply accepts either item_id/user_id or --rank, not both');
|
|
28
|
+
}
|
|
29
|
+
if (rank === 0 && hasItemId !== hasUserId) {
|
|
30
|
+
throw new ArgumentError('xianyu reply requires both item_id and user_id, or --rank from xianyu inbox');
|
|
31
|
+
}
|
|
32
|
+
if (rank === 0 && !hasItemId && !hasUserId) {
|
|
33
|
+
throw new ArgumentError('xianyu reply requires item_id/user_id or --rank from xianyu inbox');
|
|
34
|
+
}
|
|
35
|
+
const hasIds = hasItemId && hasUserId;
|
|
36
|
+
const itemId = hasIds ? normalizeNumericId(kwargs.item_id, 'item_id', '1038951278192') : '';
|
|
37
|
+
const userId = hasIds ? normalizeNumericId(kwargs.user_id, 'user_id', '3650092411') : '';
|
|
38
|
+
const text = requireText(kwargs.text, 'xianyu reply --text');
|
|
39
|
+
const url = hasIds ? buildChatUrl(itemId, userId) : '';
|
|
40
|
+
if (hasIds) {
|
|
41
|
+
await page.goto(url);
|
|
42
|
+
} else {
|
|
43
|
+
if (!page.getCurrentUrl || !/https:\/\/www\.goofish\.com\/im\b/.test(await page.getCurrentUrl())) {
|
|
44
|
+
await page.goto('https://www.goofish.com/im');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await page.wait(2);
|
|
48
|
+
if (rank > 0) {
|
|
49
|
+
requireClickResult(await page.evaluate(buildClickInboxConversationEvaluate(rank - 1)), 'reply rank click');
|
|
50
|
+
await page.wait(2);
|
|
51
|
+
}
|
|
52
|
+
const state = requireEvaluateObject(await page.evaluate(buildExtractChatStateEvaluate()), 'reply');
|
|
53
|
+
if (state?.requiresAuth) {
|
|
54
|
+
throw new AuthRequiredError('www.goofish.com', 'Xianyu reply requires a logged-in browser session');
|
|
55
|
+
}
|
|
56
|
+
if (!state?.can_input) {
|
|
57
|
+
throw selectorError('闲鱼聊天输入框', '未找到可用的聊天输入框,请确认该会话页已正确加载');
|
|
58
|
+
}
|
|
59
|
+
const sent = requireEvaluateObject(await page.evaluate(buildSendMessageEvaluate(text)), 'reply send');
|
|
60
|
+
if (!sent?.ok) {
|
|
61
|
+
throw new CommandExecutionError(`Xianyu reply did not observe the sent message: ${sent?.reason || 'unknown-reason'}`);
|
|
62
|
+
}
|
|
63
|
+
await page.wait(1);
|
|
64
|
+
return [{
|
|
65
|
+
status: 'sent',
|
|
66
|
+
peer_name: state.peer_name || '',
|
|
67
|
+
item_title: state.item_title || '',
|
|
68
|
+
price: state.price || '',
|
|
69
|
+
location: state.location || '',
|
|
70
|
+
message: text,
|
|
71
|
+
peer_user_id: userId,
|
|
72
|
+
item_id: itemId,
|
|
73
|
+
url: url || '',
|
|
74
|
+
item_url: state.item_url || '',
|
|
75
|
+
}];
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const __test__ = {
|
|
80
|
+
buildChatUrl,
|
|
81
|
+
buildSendMessageEvaluate,
|
|
82
|
+
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
10
10
|
*/
|
|
11
11
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
12
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
12
13
|
const NOTE_DETAIL_DATETIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/;
|
|
13
14
|
const NOTE_DETAIL_METRICS = [
|
|
14
15
|
{ label: '曝光数', section: '基础数据' },
|
|
@@ -337,7 +338,7 @@ cli({
|
|
|
337
338
|
const rows = await fetchCreatorNoteDetailRows(page, noteId);
|
|
338
339
|
const hasCoreMetric = rows.some((row) => row.section !== '笔记信息' && row.value);
|
|
339
340
|
if (!hasCoreMetric) {
|
|
340
|
-
throw new
|
|
341
|
+
throw new EmptyResultError('xiaohongshu creator-note-detail', 'No note detail data found. Check note_id and login status for creator.xiaohongshu.com.');
|
|
341
342
|
}
|
|
342
343
|
return rows;
|
|
343
344
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
4
|
import { appendAudienceRows, appendTrendRows, parseCreatorNoteDetailDomData, parseCreatorNoteDetailText } from './creator-note-detail.js';
|
|
4
5
|
import './creator-note-detail.js';
|
|
@@ -288,4 +289,14 @@ describe('xiaohongshu creator-note-detail', () => {
|
|
|
288
289
|
expect(page.wait).toHaveBeenCalledWith(expect.objectContaining({ time: expect.any(Number) }));
|
|
289
290
|
expect(page.wait.mock.calls.length).toBeGreaterThanOrEqual(4);
|
|
290
291
|
});
|
|
292
|
+
it('throws EmptyResultError when the detail page exposes no metrics', async () => {
|
|
293
|
+
const cmd = getRegistry().get('xiaohongshu/creator-note-detail');
|
|
294
|
+
const page = createPageMock(undefined);
|
|
295
|
+
page.evaluate = vi.fn()
|
|
296
|
+
.mockResolvedValueOnce(null)
|
|
297
|
+
.mockResolvedValueOnce('笔记数据详情\n暂无数据')
|
|
298
|
+
.mockResolvedValue(null);
|
|
299
|
+
|
|
300
|
+
await expect(cmd.func(page, { 'note-id': 'demo-note-id' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
301
|
+
});
|
|
291
302
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* returns one summary row per note, suitable for quick review or downstream JSON use.
|
|
6
6
|
*/
|
|
7
7
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
8
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
8
9
|
import { fetchCreatorNotes } from './creator-notes.js';
|
|
9
10
|
import { fetchCreatorNoteDetailRows } from './creator-note-detail.js';
|
|
10
11
|
function findDetailValue(rows, metric) {
|
|
@@ -61,7 +62,7 @@ cli({
|
|
|
61
62
|
const limit = kwargs.limit || 3;
|
|
62
63
|
const notes = await fetchCreatorNotes(page, limit);
|
|
63
64
|
if (!notes.length) {
|
|
64
|
-
throw new
|
|
65
|
+
throw new EmptyResultError('xiaohongshu creator-notes-summary', 'No notes found. Ensure you are logged into creator.xiaohongshu.com and the account has published notes.');
|
|
65
66
|
}
|
|
66
67
|
const results = [];
|
|
67
68
|
for (const [index, note] of notes.entries()) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { summarizeCreatorNote } from './creator-notes-summary.js';
|
|
3
4
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
5
|
import * as creatorNotesModule from './creator-notes.js';
|
|
@@ -84,4 +85,10 @@ describe('xiaohongshu creator-notes-summary', () => {
|
|
|
84
85
|
expect(page.wait).toHaveBeenCalledWith(expect.objectContaining({ time: expect.any(Number) }));
|
|
85
86
|
expect(page.wait.mock.calls).toHaveLength(1);
|
|
86
87
|
});
|
|
88
|
+
it('throws EmptyResultError when there are no notes to summarize', async () => {
|
|
89
|
+
const cmd = getRegistry().get('xiaohongshu/creator-notes-summary');
|
|
90
|
+
vi.spyOn(creatorNotesModule, 'fetchCreatorNotes').mockResolvedValue([]);
|
|
91
|
+
|
|
92
|
+
await expect(cmd.func({ wait: vi.fn() }, { limit: 2 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
93
|
+
});
|
|
87
94
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
9
9
|
*/
|
|
10
10
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
11
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
11
12
|
const DATE_LINE_RE = /^发布于 (\d{4}年\d{2}月\d{2}日 \d{2}:\d{2})$/;
|
|
12
13
|
const METRIC_LINE_RE = /^\d+$/;
|
|
13
14
|
const VISIBILITY_LINE_RE = /可见$/;
|
|
@@ -210,7 +211,7 @@ cli({
|
|
|
210
211
|
const limit = kwargs.limit || 20;
|
|
211
212
|
const notes = await fetchCreatorNotes(page, limit);
|
|
212
213
|
if (!Array.isArray(notes) || notes.length === 0) {
|
|
213
|
-
throw new
|
|
214
|
+
throw new EmptyResultError('xiaohongshu creator-notes', 'No notes found. Ensure you are logged into creator.xiaohongshu.com and the account has published notes.');
|
|
214
215
|
}
|
|
215
216
|
return notes
|
|
216
217
|
.slice(0, limit)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
4
|
import { parseCreatorNoteIdsFromHtml, parseCreatorNotesText } from './creator-notes.js';
|
|
4
5
|
import './creator-notes.js';
|
|
@@ -189,4 +190,15 @@ describe('xiaohongshu creator-notes', () => {
|
|
|
189
190
|
'dddddddddddddddddddddddd',
|
|
190
191
|
]);
|
|
191
192
|
});
|
|
193
|
+
it('throws EmptyResultError when the creator account has no notes', async () => {
|
|
194
|
+
const cmd = getRegistry().get('xiaohongshu/creator-notes');
|
|
195
|
+
const page = createPageMock(undefined);
|
|
196
|
+
page.evaluate = vi.fn()
|
|
197
|
+
.mockResolvedValueOnce(undefined)
|
|
198
|
+
.mockResolvedValueOnce(undefined)
|
|
199
|
+
.mockResolvedValueOnce([])
|
|
200
|
+
.mockResolvedValueOnce({ text: '', html: '' });
|
|
201
|
+
|
|
202
|
+
await expect(cmd.func(page, { limit: 1 })).rejects.toBeInstanceOf(EmptyResultError);
|
|
203
|
+
});
|
|
192
204
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Requires: logged into creator.xiaohongshu.com in Chrome.
|
|
9
9
|
*/
|
|
10
10
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
11
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
11
12
|
cli({
|
|
12
13
|
site: 'xiaohongshu',
|
|
13
14
|
name: 'creator-stats',
|
|
@@ -52,7 +53,7 @@ cli({
|
|
|
52
53
|
}
|
|
53
54
|
const stats = data.data[period];
|
|
54
55
|
if (!stats) {
|
|
55
|
-
throw new
|
|
56
|
+
throw new EmptyResultError('xiaohongshu creator-stats', `No data for period "${period}". Available: ${Object.keys(data.data).join(', ')}`);
|
|
56
57
|
}
|
|
57
58
|
// Format daily trend as sparkline-like summary
|
|
58
59
|
const formatTrend = (list) => {
|