1- import { AuthRequiredError , selectorError } from '@jackwener/opencli/errors' ;
1+ import { AuthRequiredError , CommandExecutionError , selectorError } from '@jackwener/opencli/errors' ;
22import { cli , Strategy } from '@jackwener/opencli/registry' ;
3+ import { buildChatUrl , buildExtractChatStateEvaluate , buildSendMessageEvaluate , requireEvaluateObject } from './im.js' ;
34import { 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);
135
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- }
926cli ( {
937 site : 'xianyu' ,
948 name : 'chat' ,
11125 const text = String ( kwargs . text || '' ) . trim ( ) ;
11226 await page . goto ( url ) ;
11327 await page . wait ( 2 ) ;
114- const state = await page . evaluate ( buildExtractChatStateEvaluate ( ) ) ;
28+ const state = requireEvaluateObject ( await page . evaluate ( buildExtractChatStateEvaluate ( ) ) , 'chat' ) ;
11529 if ( state ?. requiresAuth ) {
11630 throw new AuthRequiredError ( 'www.goofish.com' , 'Xianyu chat requires a logged-in browser session' ) ;
11731 }
@@ -120,37 +34,38 @@ cli({
12034 }
12135 if ( ! text ) {
12236 return [ {
123- status : 'ready' ,
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' ,
14238 peer_name : state . peer_name || '' ,
14339 item_title : state . item_title || '' ,
14440 price : state . price || '' ,
14541 location : state . location || '' ,
146- message : text ,
42+ message : ( state . visible_messages || [ ] ) . slice ( - 1 ) [ 0 ] || '' ,
14743 peer_user_id : userId ,
14844 item_id : itemId ,
14945 url,
15046 item_url : state . item_url || '' ,
15147 } ] ;
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+ } ] ;
15266 } ,
15367} ) ;
68+
15469export const __test__ = {
15570 normalizeNumericId,
15671 buildChatUrl,
0 commit comments