11/**
22 * BOSS直聘 batchgreet — batch greet recommended candidates.
3- *
4- * Combines recommend (greetRecSortList) + greet (UI automation).
5- * Sends greeting messages to multiple candidates sequentially.
63 */
74import { cli , Strategy } from '../../registry.js' ;
8- import type { IPage } from '../../types.js' ;
5+ import {
6+ requirePage , navigateToChat , fetchRecommendList ,
7+ clickCandidateInList , typeAndSendMessage , verbose ,
8+ } from './common.js' ;
99
1010cli ( {
1111 site : 'boss' ,
@@ -20,44 +20,18 @@ cli({
2020 { name : 'text' , default : '' , help : 'Custom greeting message (uses default if empty)' } ,
2121 ] ,
2222 columns : [ 'name' , 'status' , 'detail' ] ,
23- func : async ( page : IPage | null , kwargs ) => {
24- if ( ! page ) throw new Error ( 'Browser page required' ) ;
23+ func : async ( page , kwargs ) => {
24+ requirePage ( page ) ;
2525
2626 const filterJobId = kwargs [ 'job-id' ] || '' ;
2727 const limit = kwargs . limit || 5 ;
2828 const text = kwargs . text || '你好,请问您对这个职位感兴趣吗?' ;
2929
30- if ( process . env . OPENCLI_VERBOSE ) {
31- console . error ( `[opencli:boss] Batch greeting up to ${ limit } candidates...` ) ;
32- }
33-
34- await page . goto ( 'https://www.zhipin.com/web/chat/index' ) ;
35- await page . wait ( { time : 3 } ) ;
30+ verbose ( `Batch greeting up to ${ limit } candidates...` ) ;
3631
37- // Get recommended candidates
38- const listData : any = await page . evaluate ( `
39- async () => {
40- return new Promise((resolve, reject) => {
41- const xhr = new XMLHttpRequest();
42- xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
43- xhr.withCredentials = true;
44- xhr.timeout = 15000;
45- xhr.setRequestHeader('Accept', 'application/json');
46- xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
47- xhr.onerror = () => reject(new Error('Network Error'));
48- xhr.send();
49- });
50- }
51- ` ) ;
32+ await navigateToChat ( page , 3 ) ;
5233
53- if ( listData . code !== 0 ) {
54- if ( listData . code === 7 || listData . code === 37 ) {
55- throw new Error ( 'Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。' ) ;
56- }
57- throw new Error ( `获取推荐列表失败: ${ listData . message } ` ) ;
58- }
59-
60- let candidates = listData . zpData ?. friendList || [ ] ;
34+ let candidates = await fetchRecommendList ( page ) ;
6135 if ( filterJobId ) {
6236 candidates = candidates . filter ( ( f : any ) => f . encryptJobId === filterJobId ) ;
6337 }
@@ -74,88 +48,21 @@ cli({
7448 const friendName = candidate . name || '候选人' ;
7549
7650 try {
77- // Click on candidate
78- const clicked : any = await page . evaluate ( `
79- async () => {
80- const item = document.querySelector('#_${ numericUid } -0') || document.querySelector('[id^="_${ numericUid } "]');
81- if (item) {
82- item.click();
83- return { clicked: true };
84- }
85- const items = document.querySelectorAll('.geek-item');
86- for (const el of items) {
87- if (el.id && el.id.startsWith('_${ numericUid } ')) {
88- el.click();
89- return { clicked: true };
90- }
91- }
92- return { clicked: false };
93- }
94- ` ) ;
95-
96- if ( ! clicked . clicked ) {
51+ const clicked = await clickCandidateInList ( page , numericUid ) ;
52+ if ( ! clicked ) {
9753 results . push ( { name : friendName , status : '❌ 跳过' , detail : '在聊天列表中未找到' } ) ;
9854 continue ;
9955 }
10056
10157 await page . wait ( { time : 2 } ) ;
10258
103- // Type message
104- const typed : any = await page . evaluate ( `
105- async () => {
106- const selectors = [
107- '.chat-editor [contenteditable="true"]',
108- '.chat-input [contenteditable="true"]',
109- '[contenteditable="true"]',
110- 'textarea',
111- ];
112- for (const sel of selectors) {
113- const el = document.querySelector(sel);
114- if (el && el.offsetParent !== null) {
115- el.focus();
116- if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
117- el.value = ${ JSON . stringify ( text ) } ;
118- el.dispatchEvent(new Event('input', { bubbles: true }));
119- } else {
120- el.textContent = '';
121- el.focus();
122- document.execCommand('insertText', false, ${ JSON . stringify ( text ) } );
123- el.dispatchEvent(new Event('input', { bubbles: true }));
124- }
125- return { found: true };
126- }
127- }
128- return { found: false };
129- }
130- ` ) ;
131-
132- if ( ! typed . found ) {
59+ const sent = await typeAndSendMessage ( page , text ) ;
60+ if ( ! sent ) {
13361 results . push ( { name : friendName , status : '❌ 失败' , detail : '找不到消息输入框' } ) ;
13462 continue ;
13563 }
13664
137- await page . wait ( { time : 0.5 } ) ;
138-
139- // Click send
140- const sent : any = await page . evaluate ( `
141- async () => {
142- const btn = document.querySelector('.conversation-editor .submit')
143- || document.querySelector('.submit-content .submit')
144- || document.querySelector('.conversation-operate .submit');
145- if (btn) {
146- btn.click();
147- return { clicked: true };
148- }
149- return { clicked: false };
150- }
151- ` ) ;
152-
153- if ( ! sent . clicked ) {
154- await page . pressKey ( 'Enter' ) ;
155- }
156-
15765 await page . wait ( { time : 1.5 } ) ;
158-
15966 results . push ( { name : friendName , status : '✅ 已发送' , detail : text } ) ;
16067 } catch ( e : any ) {
16168 results . push ( { name : friendName , status : '❌ 失败' , detail : e . message ?. substring ( 0 , 80 ) || '未知错误' } ) ;
0 commit comments