@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
package/clis/xianyu/chat.js
CHANGED
|
@@ -1,94 +1,8 @@
|
|
|
1
|
-
import { AuthRequiredError, selectorError } from '@jackwener/opencli/errors';
|
|
1
|
+
import { AuthRequiredError, CommandExecutionError, selectorError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import { buildChatUrl, buildExtractChatStateEvaluate, buildSendMessageEvaluate, requireEvaluateObject } from './im.js';
|
|
3
4
|
import { normalizeNumericId } from './utils.js';
|
|
4
|
-
function buildChatUrl(itemId, peerUserId) {
|
|
5
|
-
return `https://www.goofish.com/im?itemId=${encodeURIComponent(itemId)}&peerUserId=${encodeURIComponent(peerUserId)}`;
|
|
6
|
-
}
|
|
7
|
-
function buildExtractChatStateEvaluate() {
|
|
8
|
-
return `
|
|
9
|
-
(() => {
|
|
10
|
-
const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
11
|
-
const bodyText = document.body?.innerText || '';
|
|
12
|
-
const requiresAuth = /请先登录|登录后/.test(bodyText);
|
|
13
5
|
|
|
14
|
-
const textarea = document.querySelector('textarea');
|
|
15
|
-
const normalizeBtn = (s) => (s || '').replace(/\\s+/g, '').trim();
|
|
16
|
-
const sendButton = Array.from(document.querySelectorAll('button'))
|
|
17
|
-
.find((btn) => normalizeBtn(btn.textContent || '') === '发送');
|
|
18
|
-
const topbar = document.querySelector('[class*="message-topbar"]');
|
|
19
|
-
const itemCard = Array.from(document.querySelectorAll('a[href*="/item?id="]'))
|
|
20
|
-
.find((el) => el.closest('main'));
|
|
21
|
-
const itemTitleNode =
|
|
22
|
-
document.querySelector('[class*="container"] [class*="title"]')
|
|
23
|
-
|| document.querySelector('[class*="item-main-info"] [class*="desc"]')
|
|
24
|
-
|| document.querySelector('[class*="headSkuInfo"]')
|
|
25
|
-
|| itemCard?.querySelector('[class*="title"]')
|
|
26
|
-
|| itemCard?.previousElementSibling?.querySelector?.('[class*="title"]');
|
|
27
|
-
|
|
28
|
-
const messageRoot = document.querySelector('#message-list-scrollable');
|
|
29
|
-
const visibleMessages = Array.from(
|
|
30
|
-
(messageRoot || document).querySelectorAll('[class*="message"], [class*="msg"], [class*="bubble"]')
|
|
31
|
-
).map((el) => clean(el.textContent || ''))
|
|
32
|
-
.filter(Boolean)
|
|
33
|
-
.filter((text) => !['发送', '闲鱼号', '立即购买'].includes(text))
|
|
34
|
-
.filter((text) => !/^消息\\d*\\+?$/.test(text))
|
|
35
|
-
.slice(-20);
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
requiresAuth,
|
|
39
|
-
title: clean(document.title || ''),
|
|
40
|
-
peer_name: clean(topbar?.querySelector('[class*="text1"]')?.textContent || ''),
|
|
41
|
-
peer_masked_id: clean(topbar?.querySelector('[class*="text2"]')?.textContent || '').replace(/^\\(|\\)$/g, ''),
|
|
42
|
-
item_title: clean(itemTitleNode?.textContent || ''),
|
|
43
|
-
item_url: itemCard?.href || '',
|
|
44
|
-
price: clean(itemCard?.querySelector('[class*="money"]')?.textContent || ''),
|
|
45
|
-
location: clean(itemCard?.querySelector('[class*="delivery"] + [class*="delivery"], [class*="delivery"]:last-child')?.textContent || ''),
|
|
46
|
-
can_input: Boolean(textarea && !textarea.disabled),
|
|
47
|
-
can_send: Boolean(sendButton),
|
|
48
|
-
visible_messages: visibleMessages,
|
|
49
|
-
};
|
|
50
|
-
})()
|
|
51
|
-
`;
|
|
52
|
-
}
|
|
53
|
-
function buildSendMessageEvaluate(text) {
|
|
54
|
-
return `
|
|
55
|
-
(async () => {
|
|
56
|
-
const clean = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
57
|
-
const textarea = document.querySelector('textarea');
|
|
58
|
-
if (!textarea || textarea.disabled) {
|
|
59
|
-
return { ok: false, reason: 'input-not-found' };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
63
|
-
if (!setter) {
|
|
64
|
-
return { ok: false, reason: 'textarea-setter-not-found' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Click textarea first to activate chat and trigger send button to appear
|
|
68
|
-
textarea.click();
|
|
69
|
-
textarea.focus();
|
|
70
|
-
setter.call(textarea, ${JSON.stringify(text)});
|
|
71
|
-
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
72
|
-
textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
|
73
|
-
|
|
74
|
-
// Poll up to 3s for send button (may appear after textarea interaction)
|
|
75
|
-
const normalizeBtn = (s) => (s || '').replace(/\\s+/g, '').trim();
|
|
76
|
-
let sendButton = null;
|
|
77
|
-
for (let i = 0; i < 30; i++) {
|
|
78
|
-
sendButton = Array.from(document.querySelectorAll('button'))
|
|
79
|
-
.find((btn) => normalizeBtn(btn.textContent || '') === '发送');
|
|
80
|
-
if (sendButton) break;
|
|
81
|
-
await new Promise(r => setTimeout(r, 100));
|
|
82
|
-
}
|
|
83
|
-
if (!sendButton) {
|
|
84
|
-
return { ok: false, reason: 'send-button-not-found' };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
sendButton.click();
|
|
88
|
-
return { ok: true };
|
|
89
|
-
})()
|
|
90
|
-
`;
|
|
91
|
-
}
|
|
92
6
|
cli({
|
|
93
7
|
site: 'xianyu',
|
|
94
8
|
name: 'chat',
|
|
@@ -111,7 +25,7 @@ cli({
|
|
|
111
25
|
const text = String(kwargs.text || '').trim();
|
|
112
26
|
await page.goto(url);
|
|
113
27
|
await page.wait(2);
|
|
114
|
-
const state = await page.evaluate(buildExtractChatStateEvaluate());
|
|
28
|
+
const state = requireEvaluateObject(await page.evaluate(buildExtractChatStateEvaluate()), 'chat');
|
|
115
29
|
if (state?.requiresAuth) {
|
|
116
30
|
throw new AuthRequiredError('www.goofish.com', 'Xianyu chat requires a logged-in browser session');
|
|
117
31
|
}
|
|
@@ -120,37 +34,38 @@ cli({
|
|
|
120
34
|
}
|
|
121
35
|
if (!text) {
|
|
122
36
|
return [{
|
|
123
|
-
|
|
124
|
-
peer_name: state.peer_name || '',
|
|
125
|
-
item_title: state.item_title || '',
|
|
126
|
-
price: state.price || '',
|
|
127
|
-
location: state.location || '',
|
|
128
|
-
message: (state.visible_messages || []).slice(-1)[0] || '',
|
|
129
|
-
peer_user_id: userId,
|
|
130
|
-
item_id: itemId,
|
|
131
|
-
url,
|
|
132
|
-
item_url: state.item_url || '',
|
|
133
|
-
}];
|
|
134
|
-
}
|
|
135
|
-
const sent = await page.evaluate(buildSendMessageEvaluate(text));
|
|
136
|
-
if (!sent?.ok) {
|
|
137
|
-
throw selectorError('闲鱼发送按钮', `消息发送失败:${sent?.reason || 'unknown-reason'}`);
|
|
138
|
-
}
|
|
139
|
-
await page.wait(1);
|
|
140
|
-
return [{
|
|
141
|
-
status: 'sent',
|
|
37
|
+
status: 'ready',
|
|
142
38
|
peer_name: state.peer_name || '',
|
|
143
39
|
item_title: state.item_title || '',
|
|
144
40
|
price: state.price || '',
|
|
145
41
|
location: state.location || '',
|
|
146
|
-
message:
|
|
42
|
+
message: (state.visible_messages || []).slice(-1)[0] || '',
|
|
147
43
|
peer_user_id: userId,
|
|
148
44
|
item_id: itemId,
|
|
149
45
|
url,
|
|
150
46
|
item_url: state.item_url || '',
|
|
151
47
|
}];
|
|
48
|
+
}
|
|
49
|
+
const sent = requireEvaluateObject(await page.evaluate(buildSendMessageEvaluate(text)), 'chat send');
|
|
50
|
+
if (!sent?.ok) {
|
|
51
|
+
throw new CommandExecutionError(`Xianyu chat did not observe the sent message: ${sent?.reason || 'unknown-reason'}`);
|
|
52
|
+
}
|
|
53
|
+
await page.wait(1);
|
|
54
|
+
return [{
|
|
55
|
+
status: 'sent',
|
|
56
|
+
peer_name: state.peer_name || '',
|
|
57
|
+
item_title: state.item_title || '',
|
|
58
|
+
price: state.price || '',
|
|
59
|
+
location: state.location || '',
|
|
60
|
+
message: text,
|
|
61
|
+
peer_user_id: userId,
|
|
62
|
+
item_id: itemId,
|
|
63
|
+
url,
|
|
64
|
+
item_url: state.item_url || '',
|
|
65
|
+
}];
|
|
152
66
|
},
|
|
153
67
|
});
|
|
68
|
+
|
|
154
69
|
export const __test__ = {
|
|
155
70
|
normalizeNumericId,
|
|
156
71
|
buildChatUrl,
|
package/clis/xianyu/chat.test.js
CHANGED
|
@@ -41,6 +41,7 @@ describe('xianyu chat helpers', () => {
|
|
|
41
41
|
const result = await runBrowserScript(`
|
|
42
42
|
<main>
|
|
43
43
|
<textarea></textarea>
|
|
44
|
+
<div id="message-list-scrollable"></div>
|
|
44
45
|
</main>
|
|
45
46
|
`, __test__.buildSendMessageEvaluate('还在吗?'), {
|
|
46
47
|
beforeEval(window) {
|
|
@@ -53,6 +54,10 @@ describe('xianyu chat helpers', () => {
|
|
|
53
54
|
button.textContent = '发 送';
|
|
54
55
|
button.addEventListener('click', () => {
|
|
55
56
|
sendClicked = true;
|
|
57
|
+
const row = window.document.createElement('div');
|
|
58
|
+
row.className = 'message-row';
|
|
59
|
+
row.innerHTML = '<div class="message-text">还在吗?</div>';
|
|
60
|
+
window.document.querySelector('#message-list-scrollable').append(row);
|
|
56
61
|
});
|
|
57
62
|
window.document.body.append(button);
|
|
58
63
|
});
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_INBOX_LIMIT = 20;
|
|
4
|
+
export const MAX_INBOX_LIMIT = 100;
|
|
5
|
+
export const DEFAULT_MESSAGE_LIMIT = 50;
|
|
6
|
+
export const MAX_MESSAGE_LIMIT = 200;
|
|
7
|
+
|
|
8
|
+
export function normalizeLimit(value, defaultValue = DEFAULT_INBOX_LIMIT, maxValue = MAX_INBOX_LIMIT, label = 'limit') {
|
|
9
|
+
const raw = String(value ?? '').trim();
|
|
10
|
+
if (!raw) return defaultValue;
|
|
11
|
+
if (!/^\d+$/.test(raw)) {
|
|
12
|
+
throw new ArgumentError(`xianyu ${label} must be an integer between 1 and ${maxValue}`);
|
|
13
|
+
}
|
|
14
|
+
const n = Number(raw);
|
|
15
|
+
if (!Number.isSafeInteger(n) || n < 1 || n > maxValue) {
|
|
16
|
+
throw new ArgumentError(`xianyu ${label} must be an integer between 1 and ${maxValue}`);
|
|
17
|
+
}
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeRank(value) {
|
|
22
|
+
const raw = String(value ?? '').trim();
|
|
23
|
+
if (!raw) return 0;
|
|
24
|
+
if (!/^\d+$/.test(raw)) {
|
|
25
|
+
throw new ArgumentError('xianyu rank must be a positive integer from xianyu inbox');
|
|
26
|
+
}
|
|
27
|
+
const n = Number(raw);
|
|
28
|
+
if (!Number.isSafeInteger(n) || n < 1) {
|
|
29
|
+
throw new ArgumentError('xianyu rank must be a positive integer from xianyu inbox');
|
|
30
|
+
}
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function requireText(value, label) {
|
|
35
|
+
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
36
|
+
if (!text) {
|
|
37
|
+
throw new ArgumentError(`${label} cannot be empty`);
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function requireEvaluateObject(payload, label) {
|
|
43
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
44
|
+
throw new CommandExecutionError(`Xianyu ${label} returned malformed browser payload`);
|
|
45
|
+
}
|
|
46
|
+
return payload;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function requireClickResult(payload, label) {
|
|
50
|
+
const result = requireEvaluateObject(payload, label);
|
|
51
|
+
if (result.ok !== true) {
|
|
52
|
+
throw new CommandExecutionError(`Xianyu ${label} failed: ${result.reason || 'unknown-reason'}`);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildChatUrl(itemId, peerUserId) {
|
|
58
|
+
return `https://www.goofish.com/im?itemId=${encodeURIComponent(itemId)}&peerUserId=${encodeURIComponent(peerUserId)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildInboxUrl() {
|
|
62
|
+
return 'https://www.goofish.com/im';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildClickInboxConversationEvaluate(index) {
|
|
66
|
+
return `
|
|
67
|
+
(() => {
|
|
68
|
+
const rows = Array.from(document.querySelectorAll('#conv-list-scrollable [class*="conversation-item"], a[href*="/im"], a[href*="itemId="][href*="peerUserId="]'));
|
|
69
|
+
const row = rows[${index}];
|
|
70
|
+
if (!row) return { ok: false, reason: 'row-not-found' };
|
|
71
|
+
row.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: window }));
|
|
72
|
+
row.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
|
|
73
|
+
row.click();
|
|
74
|
+
row.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
|
|
75
|
+
return { ok: true };
|
|
76
|
+
})()
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildReadCurrentConversationUrlEvaluate() {
|
|
81
|
+
return `
|
|
82
|
+
(() => {
|
|
83
|
+
const url = location.href || '';
|
|
84
|
+
const params = new URL(url).searchParams;
|
|
85
|
+
return {
|
|
86
|
+
url,
|
|
87
|
+
item_id: params.get('itemId') || '',
|
|
88
|
+
peer_user_id: params.get('peerUserId') || '',
|
|
89
|
+
};
|
|
90
|
+
})()
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildExtractInboxEvaluate(limit) {
|
|
95
|
+
return `
|
|
96
|
+
(() => {
|
|
97
|
+
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
98
|
+
const bodyText = document.body?.innerText || '';
|
|
99
|
+
const requiresAuth = /请先登录|登录后|璇峰厛鐧诲綍|鐧诲綍鍚?/.test(bodyText);
|
|
100
|
+
const blocked = /验证码|安全验证|异常访问|楠岃瘉鐮亅瀹夊叏楠岃瘉|寮傚父璁块棶/.test(bodyText);
|
|
101
|
+
|
|
102
|
+
const absoluteUrl = (url) => {
|
|
103
|
+
try {
|
|
104
|
+
return new URL(url, location.href).href;
|
|
105
|
+
} catch {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const readId = (url, key) => {
|
|
111
|
+
try {
|
|
112
|
+
return new URL(url, location.href).searchParams.get(key) || '';
|
|
113
|
+
} catch {
|
|
114
|
+
const match = String(url || '').match(new RegExp('[?&]' + key + '=(\\\\d+)'));
|
|
115
|
+
return match ? match[1] : '';
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const pick = (root, selectors) => {
|
|
120
|
+
for (const selector of selectors) {
|
|
121
|
+
const node = root.querySelector(selector);
|
|
122
|
+
const text = clean(node?.getAttribute?.('title') || node?.textContent || '');
|
|
123
|
+
if (text) return text;
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const leafTexts = (root) => Array.from(root.querySelectorAll('div, span'))
|
|
129
|
+
.filter((node) => !Array.from(node.children || []).some((child) => ['DIV', 'SPAN'].includes(child.tagName)))
|
|
130
|
+
.map((node) => clean(node.textContent || ''))
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
|
|
133
|
+
const looksLikeTime = (text) => /^(刚刚|\\d+分钟前|\\d+小时前|\\d+天前|昨天|前天|\\d{1,2}:\\d{2}|\\d{4}-\\d{1,2}-\\d{1,2})$/.test(text);
|
|
134
|
+
|
|
135
|
+
const links = Array.from(document.querySelectorAll('a[href*="/im"], a[href*="itemId="][href*="peerUserId="]'));
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const items = [];
|
|
138
|
+
for (const link of links) {
|
|
139
|
+
const href = link.href || link.getAttribute('href') || '';
|
|
140
|
+
const itemId = readId(href, 'itemId');
|
|
141
|
+
const peerUserId = readId(href, 'peerUserId');
|
|
142
|
+
if (!itemId || !peerUserId) continue;
|
|
143
|
+
const key = itemId + ':' + peerUserId;
|
|
144
|
+
if (seen.has(key)) continue;
|
|
145
|
+
seen.add(key);
|
|
146
|
+
|
|
147
|
+
const root = link.closest('[class*="conversation"], [class*="session"], [class*="contact"], [class*="chat"], li, [role="listitem"]') || link;
|
|
148
|
+
const peerName = pick(root, ['[class*="name"]', '[class*="nick"]', '[class*="user"]', '[class*="text1"]']);
|
|
149
|
+
const itemTitle = pick(root, ['[class*="title"]', '[class*="item"] [class*="desc"]', '[class*="desc"]']);
|
|
150
|
+
const price = pick(root, ['[class*="money"]', '[class*="price"]']);
|
|
151
|
+
const lastMessage = pick(root, ['[class*="message"]', '[class*="msg"]', '[class*="content"]', '[class*="summary"]']);
|
|
152
|
+
const rootText = clean(root.textContent || '');
|
|
153
|
+
const unreadText = pick(root, ['[class*="badge"]', '[class*="unread"]', '[class*="red"]']);
|
|
154
|
+
const unreadCount = Number.parseInt(unreadText.replace(/\\D/g, ''), 10) || (unreadText ? 1 : 0);
|
|
155
|
+
|
|
156
|
+
items.push({
|
|
157
|
+
row_index: items.length,
|
|
158
|
+
peer_name: peerName,
|
|
159
|
+
peer_user_id: peerUserId,
|
|
160
|
+
item_id: itemId,
|
|
161
|
+
item_title: itemTitle,
|
|
162
|
+
price,
|
|
163
|
+
last_message: lastMessage || rootText,
|
|
164
|
+
unread: unreadCount > 0 || /unread|未读/.test(String(root.className || '') + ' ' + rootText),
|
|
165
|
+
unread_count: unreadCount,
|
|
166
|
+
url: absoluteUrl(href),
|
|
167
|
+
});
|
|
168
|
+
if (items.length >= ${limit}) break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!items.length) {
|
|
172
|
+
const rows = Array.from(document.querySelectorAll('#conv-list-scrollable [class*="conversation-item"]')).slice(0, ${limit});
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
const texts = leafTexts(row)
|
|
175
|
+
.filter((text) => !/^\\d+$/.test(text) || row.querySelector('sup[title="' + text.replace(/"/g, '\\"') + '"]') == null);
|
|
176
|
+
const time = [...texts].reverse().find(looksLikeTime) || '';
|
|
177
|
+
const unreadTitle = clean(row.querySelector('sup')?.getAttribute('title') || row.querySelector('[class*="badge"], [class*="unread"]')?.textContent || '');
|
|
178
|
+
const unreadCount = Number.parseInt(unreadTitle.replace(/\\D/g, ''), 10) || (unreadTitle ? 1 : 0);
|
|
179
|
+
const peerName = texts.find((text) => text !== time && text !== unreadTitle && !/^\\[.*\\]$/.test(text)) || '';
|
|
180
|
+
const lastMessage = texts.find((text) => text !== peerName && text !== time && text !== unreadTitle) || '';
|
|
181
|
+
if (!peerName && !lastMessage) continue;
|
|
182
|
+
items.push({
|
|
183
|
+
row_index: items.length,
|
|
184
|
+
peer_name: peerName,
|
|
185
|
+
peer_user_id: '',
|
|
186
|
+
item_id: '',
|
|
187
|
+
item_title: '',
|
|
188
|
+
price: '',
|
|
189
|
+
last_message: lastMessage,
|
|
190
|
+
unread: unreadCount > 0,
|
|
191
|
+
unread_count: unreadCount,
|
|
192
|
+
url: '',
|
|
193
|
+
time,
|
|
194
|
+
});
|
|
195
|
+
if (items.length >= ${limit}) break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { requiresAuth, blocked, empty: !items.length, items };
|
|
200
|
+
})()
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function buildExtractChatStateEvaluate(limit = DEFAULT_MESSAGE_LIMIT) {
|
|
205
|
+
return `
|
|
206
|
+
(() => {
|
|
207
|
+
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
208
|
+
const bodyText = document.body?.innerText || '';
|
|
209
|
+
const requiresAuth = /请先登录|登录后|璇峰厛鐧诲綍|鐧诲綍鍚?/.test(bodyText);
|
|
210
|
+
|
|
211
|
+
const textarea = document.querySelector('textarea');
|
|
212
|
+
const normalizeBtn = (s) => String(s || '').replace(/\\s+/g, '').trim();
|
|
213
|
+
const sendButton = Array.from(document.querySelectorAll('button'))
|
|
214
|
+
.find((btn) => /^(发送|鍙戦€?)$/.test(normalizeBtn(btn.textContent || '')));
|
|
215
|
+
const topbar = document.querySelector('[class*="message-topbar"]');
|
|
216
|
+
const itemCard = Array.from(document.querySelectorAll('a[href*="/item?id="]'))
|
|
217
|
+
.find((el) => el.closest('main')) || document.querySelector('a[href*="/item?id="]');
|
|
218
|
+
const itemTitleNode =
|
|
219
|
+
document.querySelector('[class*="container"] [class*="title"]')
|
|
220
|
+
|| document.querySelector('[class*="item-main-info"] [class*="desc"]')
|
|
221
|
+
|| document.querySelector('[class*="headSkuInfo"]')
|
|
222
|
+
|| itemCard?.querySelector('[class*="title"]')
|
|
223
|
+
|| itemCard?.previousElementSibling?.querySelector?.('[class*="title"]');
|
|
224
|
+
|
|
225
|
+
const messageRoot = document.querySelector('#message-list-scrollable') || document.querySelector('[class*="message-list"]');
|
|
226
|
+
let visibleMessages = Array.from((messageRoot || document).querySelectorAll('[class*="message-row"]'))
|
|
227
|
+
.map((row) => {
|
|
228
|
+
const textNode = row.querySelector('[class*="message-text"]');
|
|
229
|
+
return clean(textNode?.textContent || '');
|
|
230
|
+
})
|
|
231
|
+
.filter(Boolean);
|
|
232
|
+
|
|
233
|
+
if (!visibleMessages.length) {
|
|
234
|
+
visibleMessages = Array.from(
|
|
235
|
+
(messageRoot || document).querySelectorAll('[class*="message"], [class*="msg"], [class*="bubble"]')
|
|
236
|
+
).map((el) => clean(el.textContent || ''));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
visibleMessages = visibleMessages
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.filter((text) => !['发送', '闲鱼号', '立即购买', '鍙戦€?', '闂查奔鍙?', '绔嬪嵆璐拱'].includes(text))
|
|
242
|
+
.filter((text) => !/^消息\\d*\\+?$/.test(text) && !/^娑堟伅\\d*\\+?$/.test(text))
|
|
243
|
+
.slice(-${limit});
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
requiresAuth,
|
|
247
|
+
title: clean(document.title || ''),
|
|
248
|
+
peer_name: clean(topbar?.querySelector('[class*="text1"]')?.textContent || ''),
|
|
249
|
+
peer_masked_id: clean(topbar?.querySelector('[class*="text2"]')?.textContent || '').replace(/^\\(|\\)$/g, ''),
|
|
250
|
+
item_title: clean(itemTitleNode?.textContent || ''),
|
|
251
|
+
item_url: itemCard?.href || '',
|
|
252
|
+
price: clean(itemCard?.querySelector('[class*="money"], [class*="price"]')?.textContent || ''),
|
|
253
|
+
location: clean(itemCard?.querySelector('[class*="delivery"] + [class*="delivery"], [class*="delivery"]:last-child')?.textContent || ''),
|
|
254
|
+
can_input: Boolean(textarea && !textarea.disabled),
|
|
255
|
+
can_send: Boolean(sendButton),
|
|
256
|
+
visible_messages: visibleMessages,
|
|
257
|
+
messages: visibleMessages.map((text, index) => ({ index: index + 1, text })),
|
|
258
|
+
};
|
|
259
|
+
})()
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function buildSendMessageEvaluate(text) {
|
|
264
|
+
return `
|
|
265
|
+
(async () => {
|
|
266
|
+
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
267
|
+
const readMessages = () => {
|
|
268
|
+
const messageRoot = document.querySelector('#message-list-scrollable') || document.querySelector('[class*="message-list"]');
|
|
269
|
+
let messages = Array.from((messageRoot || document).querySelectorAll('[class*="message-row"]'))
|
|
270
|
+
.map((row) => clean(row.querySelector('[class*="message-text"]')?.textContent || ''))
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
if (!messages.length) {
|
|
273
|
+
messages = Array.from(
|
|
274
|
+
(messageRoot || document).querySelectorAll('[class*="message"], [class*="msg"], [class*="bubble"]')
|
|
275
|
+
).map((el) => clean(el.textContent || '')).filter(Boolean);
|
|
276
|
+
}
|
|
277
|
+
return messages
|
|
278
|
+
.filter((item) => !['发送', '闲鱼号', '立即购买', '鍙戦€?', '闂查奔鍙?', '绔嬪嵆璐拱'].includes(item))
|
|
279
|
+
.filter((item) => !/^消息\\d*\\+?$/.test(item) && !/^娑堟伅\\d*\\+?$/.test(item));
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const textarea = document.querySelector('textarea');
|
|
283
|
+
if (!textarea || textarea.disabled) {
|
|
284
|
+
return { ok: false, reason: 'input-not-found' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
288
|
+
if (!setter) {
|
|
289
|
+
return { ok: false, reason: 'textarea-setter-not-found' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const beforeMessages = readMessages();
|
|
293
|
+
textarea.click();
|
|
294
|
+
textarea.focus();
|
|
295
|
+
setter.call(textarea, ${JSON.stringify(text)});
|
|
296
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
297
|
+
textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
|
298
|
+
|
|
299
|
+
const normalizeBtn = (s) => String(s || '').replace(/\\s+/g, '').trim();
|
|
300
|
+
let sendButton = null;
|
|
301
|
+
for (let i = 0; i < 30; i++) {
|
|
302
|
+
sendButton = Array.from(document.querySelectorAll('button'))
|
|
303
|
+
.find((btn) => /^(发送|鍙戦€?)$/.test(normalizeBtn(btn.textContent || '')));
|
|
304
|
+
if (sendButton) break;
|
|
305
|
+
await new Promise(r => setTimeout(r, 100));
|
|
306
|
+
}
|
|
307
|
+
if (!sendButton) {
|
|
308
|
+
return { ok: false, reason: 'send-button-not-found' };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
sendButton.click();
|
|
312
|
+
for (let i = 0; i < 30; i++) {
|
|
313
|
+
await new Promise(r => setTimeout(r, 100));
|
|
314
|
+
const afterMessages = readMessages();
|
|
315
|
+
if (afterMessages.length > beforeMessages.length && afterMessages.at(-1) === ${JSON.stringify(text)}) {
|
|
316
|
+
return { ok: true };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { ok: false, reason: 'send-postcondition-timeout' };
|
|
320
|
+
})()
|
|
321
|
+
`;
|
|
322
|
+
}
|