@reconcrap/boss-recruit-mcp 1.0.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.
@@ -0,0 +1,1646 @@
1
+ #!/usr/bin/env node
2
+ const WebSocket = require('ws');
3
+ const http = require('http');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+
8
+ const args = process.argv.slice(2).reduce((acc, arg, i, arr) => {
9
+ if (arg.startsWith('--')) {
10
+ const key = arg.slice(2);
11
+ acc[key] = arr[i + 1] && !arr[i + 1].startsWith('--') ? arr[i + 1] : true;
12
+ }
13
+ return acc;
14
+ }, {});
15
+
16
+ const baseUrl = args.baseurl || args.baseUrl;
17
+ const apiKey = args.apikey || args.apiKey;
18
+ const model = args.model;
19
+ const criteria = args.criteria;
20
+ const targetCount = parseInt(args.target || args.targetCount || '10');
21
+ const configFile = args.config || 'favorite-calibration.json';
22
+ const outputCsv = args.output || `筛选结果_${Date.now()}.csv`;
23
+
24
+ if (!baseUrl || !apiKey || !model || !criteria || !targetCount) {
25
+ console.error('Usage: node boss-cli.js --baseurl <url> --apikey <key> --model <model> --criteria <criteria> --targetCount <n> [--config <file>] [--output <csv>]');
26
+ process.exit(1);
27
+ }
28
+
29
+ function loadCalibration() {
30
+ if (!fs.existsSync(configFile)) {
31
+ console.error(`错误: 校准文件不存在: ${configFile}`);
32
+ console.error('请先运行校准脚本');
33
+ process.exit(1);
34
+ }
35
+ const content = fs.readFileSync(configFile, 'utf8').replace(/^\uFEFF/, '');
36
+ const data = JSON.parse(content);
37
+ return data.favoritePosition;
38
+ }
39
+
40
+ async function getChromeTab() {
41
+ return new Promise((resolve, reject) => {
42
+ http.get('http://localhost:9222/json/list', (res) => {
43
+ let data = '';
44
+ res.on('data', chunk => data += chunk);
45
+ res.on('end', () => {
46
+ const tabs = JSON.parse(data);
47
+ const bossTab = tabs.find(t => t.url && t.url.includes('zhipin.com'));
48
+ if (bossTab) resolve(bossTab);
49
+ else reject(new Error('未找到BOSS直聘页面'));
50
+ });
51
+ }).on('error', reject);
52
+ });
53
+ }
54
+
55
+ class CDPClient {
56
+ constructor(wsUrl) {
57
+ this.ws = new WebSocket(wsUrl);
58
+ this.msgId = 0;
59
+ this.pending = new Map();
60
+ this.networkListeners = new Map();
61
+ this.ws.on('message', (data) => {
62
+ const msg = JSON.parse(data);
63
+ if (msg.id && this.pending.has(msg.id)) {
64
+ this.pending.get(msg.id)(msg);
65
+ this.pending.delete(msg.id);
66
+ } else if (msg.method && this.networkListeners.has(msg.method)) {
67
+ this.networkListeners.get(msg.method)(msg.params);
68
+ }
69
+ });
70
+ }
71
+
72
+ send(method, params = {}) {
73
+ return new Promise((resolve, reject) => {
74
+ const id = ++this.msgId;
75
+ this.pending.set(id, (msg) => {
76
+ if (msg.result) {
77
+ const result = msg.result.result || msg.result;
78
+ resolve(result.value !== undefined ? result.value : result);
79
+ } else if (msg.error) {
80
+ reject(new Error(msg.error.message || JSON.stringify(msg.error)));
81
+ } else {
82
+ reject(new Error('Unknown CDP response: ' + JSON.stringify(msg)));
83
+ }
84
+ });
85
+ this.ws.send(JSON.stringify({ id, method, params }));
86
+ setTimeout(() => {
87
+ if (this.pending.has(id)) {
88
+ this.pending.delete(id);
89
+ resolve(null);
90
+ }
91
+ }, 10000);
92
+ });
93
+ }
94
+
95
+ on(method, callback) {
96
+ this.networkListeners.set(method, callback);
97
+ }
98
+
99
+ close() {
100
+ this.ws.close();
101
+ }
102
+ }
103
+
104
+ let capturedResumeData = null;
105
+ let resumeRequestId = null;
106
+ let favoriteActionResult = null;
107
+ let favoriteRequestId = null;
108
+ let pendingFavoriteClick = false;
109
+
110
+ async function enableNetworkInterception(cdp) {
111
+ await cdp.send('Network.enable');
112
+
113
+ cdp.on('Network.requestWillBeSent', (params) => {
114
+ if (params.request && params.request.url) {
115
+ const url = params.request.url;
116
+
117
+ if (url.includes('/wapi/zpitem/web/boss/search/geek/info')) {
118
+ resumeRequestId = params.requestId;
119
+ }
120
+ if (url.includes('userMark')) {
121
+ favoriteRequestId = params.requestId;
122
+ if (pendingFavoriteClick) {
123
+ if (url.includes('/add')) {
124
+ favoriteActionResult = 'add';
125
+ console.log(` [检测到] 收藏请求 add`);
126
+ } else if (url.includes('/del')) {
127
+ favoriteActionResult = 'del';
128
+ console.log(` [检测到] 收藏请求 del`);
129
+ }
130
+ pendingFavoriteClick = false;
131
+ }
132
+ }
133
+ if (url.includes('actionLog/common.json') && pendingFavoriteClick) {
134
+ const postData = params.request.postData;
135
+ if (postData) {
136
+ try {
137
+ const payload = JSON.parse(postData);
138
+ if (payload.action === 'star-interest-click') {
139
+ if (payload.p3 === 1) {
140
+ favoriteActionResult = 'add';
141
+ console.log(` [actionLog检测到] 添加收藏`);
142
+ } else if (payload.p3 === 0) {
143
+ favoriteActionResult = 'del';
144
+ console.log(` [actionLog检测到] 取消收藏`);
145
+ }
146
+ pendingFavoriteClick = false;
147
+ }
148
+ } catch (e) {}
149
+ }
150
+ }
151
+ }
152
+ });
153
+ cdp.on('Network.loadingFinished', (params) => {
154
+ if (params.requestId === resumeRequestId) {
155
+ setTimeout(async () => {
156
+ try {
157
+ const responseBody = await cdp.send('Network.getResponseBody', { requestId: params.requestId });
158
+ if (responseBody && responseBody.body) {
159
+ const data = JSON.parse(responseBody.body);
160
+ if (data && data.zpData) {
161
+ capturedResumeData = data.zpData;
162
+ }
163
+ }
164
+ } catch (e) {}
165
+ }, 100);
166
+ }
167
+ if (params.requestId === favoriteRequestId) {
168
+ setTimeout(async () => {
169
+ try {
170
+ const responseBody = await cdp.send('Network.getResponseBody', { requestId: params.requestId });
171
+ if (responseBody && responseBody.body) {
172
+ const url = responseBody.url || '';
173
+ if (url.includes('/add')) {
174
+ favoriteActionResult = 'add';
175
+ } else if (url.includes('/del')) {
176
+ favoriteActionResult = 'del';
177
+ }
178
+ }
179
+ } catch (e) {}
180
+ }, 100);
181
+ }
182
+ });
183
+ }
184
+
185
+ async function getResumeDataViaCDP(cdp, requestId) {
186
+ try {
187
+ const responseBody = await cdp.send('Network.getResponseBody', { requestId: requestId });
188
+ if (responseBody && responseBody.body) {
189
+ const data = JSON.parse(responseBody.body);
190
+ if (data && data.zpData) {
191
+ return data.zpData;
192
+ }
193
+ }
194
+ } catch (e) {
195
+ console.log(' CDP获取简历失败');
196
+ }
197
+ return null;
198
+ }
199
+
200
+ const jsGetList = `(function(){
201
+ var frame=window.frames['searchFrame'];
202
+ if(!frame)return JSON.stringify({error:'searchFrame not found'});
203
+ var doc=frame.document||frame.contentDocument;
204
+ if(!doc)return JSON.stringify({error:'cannot access frame'});
205
+
206
+ // 优先使用 li.card-item 选择器(与扩展一致)
207
+ var cards=doc.querySelectorAll('li.card-item');
208
+
209
+ // 备用:使用 a[data-jid][data-itemid] 选择器
210
+ if(cards.length===0){
211
+ cards=doc.querySelectorAll('a[data-jid][data-itemid]');
212
+ }
213
+
214
+ // 再备用:所有 li 元素
215
+ if(cards.length===0){
216
+ cards=doc.querySelectorAll('li');
217
+ }
218
+
219
+ return JSON.stringify({totalCards:cards.length});
220
+ })()`;
221
+
222
+ const jsGetNextCard = (idx) => '(function(idx){' +
223
+ 'try{' +
224
+ 'var frame=window.frames["searchFrame"];' +
225
+ 'if(!frame)return JSON.stringify({error:"searchFrame not found"});' +
226
+ 'var doc=frame.document||frame.contentDocument;' +
227
+ 'if(!doc)return JSON.stringify({error:"cannot access frame doc"});' +
228
+ 'var allCards=doc.querySelectorAll("li.card-item");' +
229
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("a[data-jid][data-itemid]");}' +
230
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("li");}' +
231
+ 'if(allCards.length===0){return JSON.stringify({found:false,total:0,error:"no cards found"});}' +
232
+ 'if(idx>=allCards.length){return JSON.stringify({found:false,total:allCards.length,error:"index out of range"});}' +
233
+ 'var card=null;var count=0;' +
234
+ 'for(var i=0;i<allCards.length;i++){' +
235
+ 'var c=allCards[i];' +
236
+ 'if(c&&(c.dataset||c.getAttribute)){' +
237
+ 'if(count===idx){card=c;break;}' +
238
+ 'count++;}' +
239
+ '}' +
240
+ 'if(!card){return JSON.stringify({found:false,total:allCards.length,error:"card element not found at index "+idx});}' +
241
+ 'var jid="";var itemid="";' +
242
+ 'if(card.dataset){jid=card.dataset.jid||"";itemid=card.dataset.itemid||"";}' +
243
+ 'else if(card.getAttribute){jid=card.getAttribute("data-jid")||"";itemid=card.getAttribute("data-itemid")||"";}' +
244
+ 'var cardText=card.innerText||card.textContent||"";' +
245
+ 'return JSON.stringify({found:true,index:idx,jid:jid,itemid:itemid,total:allCards.length,hasName:cardText.length>0,preview:cardText.substring(0,50)});' +
246
+ '}catch(e){return JSON.stringify({error:e.message});}' +
247
+ '})(' + idx + ')';
248
+
249
+ const jsFindNextUnprocessedCard = `(function(startIdx, processedKeys){
250
+ var frame=window.frames["searchFrame"];
251
+ if(!frame)return JSON.stringify({error:'searchFrame not found'});
252
+ var doc=frame.document||frame.contentDocument;
253
+ var allCards=doc.querySelectorAll("li.card-item");
254
+ if(allCards.length===0){allCards=doc.querySelectorAll("a[data-jid][data-itemid]");}
255
+ if(allCards.length===0){allCards=doc.querySelectorAll("li");}
256
+ if(allCards.length===0){return JSON.stringify({found:false,total:0,error:'no cards found'});}
257
+
258
+ for(var i=startIdx;i<allCards.length;i++){
259
+ var card=allCards[i];
260
+ if(!card)continue;
261
+
262
+ // 优先从 data-lid 获取唯一标识 (与扩展一致)
263
+ var linkEl=card.querySelector('a[data-lid]');
264
+ var lid='';
265
+ if(linkEl){
266
+ lid=linkEl.getAttribute('data-lid')||'';
267
+ var match=lid.match(/lookupsearchgeek\\.(\\d+)/);
268
+ if(match){lid='geek_'+match[1];}
269
+ }
270
+
271
+ // 备用: data-jid, data-geek, data-geekid
272
+ var jid='';
273
+ if(card.dataset){
274
+ jid=card.dataset.jid||card.dataset.geek||card.dataset.geekid||'';
275
+ } else if(card.getAttribute){
276
+ jid=card.getAttribute('data-jid')||card.getAttribute('data-geek')||card.getAttribute('data-geekid')||'';
277
+ }
278
+
279
+ var cardText=card.innerText||card.textContent||'';
280
+ // 生成 key: 优先用 lid > jid > (itemid+文本前20字)
281
+ var key=lid||jid;
282
+ if(!key){
283
+ var itemid='';
284
+ if(card.dataset){
285
+ itemid=card.dataset.itemid||'';
286
+ } else if(card.getAttribute){
287
+ itemid=card.getAttribute('data-itemid')||'';
288
+ }
289
+ key=itemid+'_'+cardText.substring(0,20);
290
+ }
291
+
292
+ if(processedKeys&&processedKeys.has&&processedKeys.has(key)){
293
+ continue;
294
+ }
295
+ return JSON.stringify({found:true,index:i,jid:key,lid:lid,itemid:jid,total:allCards.length,key:key,hasName:cardText.length>0});
296
+ }
297
+ return JSON.stringify({found:false,total:allCards.length,error:'all cards processed'});
298
+ })`;
299
+ const jsClickCard = (idx) => '(function(idx){' +
300
+ 'var frame=window.frames["searchFrame"];' +
301
+ 'if(!frame)return JSON.stringify({error:"searchFrame not found"});' +
302
+ 'var doc=frame.document||frame.contentDocument;' +
303
+ 'var allCards=doc.querySelectorAll("li.card-item");' +
304
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("a[data-jid][data-itemid]");}' +
305
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("li");}' +
306
+ 'if(allCards.length===0)return JSON.stringify({error:"no cards found"});' +
307
+ 'if(idx>=allCards.length)return JSON.stringify({error:"Index out of range: "+idx});' +
308
+ 'var card=null;var count=0;' +
309
+ 'for(var i=0;i<allCards.length;i++){' +
310
+ 'var c=allCards[i];' +
311
+ 'if(c&&(c.dataset||c.getAttribute)){' +
312
+ 'if(count===idx){card=c;break;}' +
313
+ 'count++;}' +
314
+ '}' +
315
+ 'if(!card)return JSON.stringify({error:"card not found at index "+idx});' +
316
+ 'if(card.click){card.click();return JSON.stringify({success:true,method:"direct-click"});}' +
317
+ 'var evt=new MouseEvent("click",{bubbles:true,cancelable:true,view:window});' +
318
+ 'card.dispatchEvent(evt);' +
319
+ 'return JSON.stringify({success:true,method:"dispatch-event"});' +
320
+ '})(' + idx + ')';
321
+
322
+ const jsGetCardPosition = (idx) => '(function(idx){' +
323
+ 'try{' +
324
+ 'var frame=window.frames["searchFrame"];' +
325
+ 'if(!frame)return JSON.stringify({error:"searchFrame not found"});' +
326
+ 'var doc=frame.document||frame.contentDocument;' +
327
+ 'var allCards=doc.querySelectorAll("li.card-item");' +
328
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("a[data-jid][data-itemid]");}' +
329
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("li");}' +
330
+ 'if(allCards.length===0)return JSON.stringify({error:"no cards found"});' +
331
+ 'var card=null;var count=0;' +
332
+ 'for(var i=0;i<allCards.length;i++){' +
333
+ 'var c=allCards[i];' +
334
+ 'if(c&&(c.dataset||c.getAttribute)){' +
335
+ 'if(count===idx){card=c;break;}' +
336
+ 'count++;}' +
337
+ '}' +
338
+ 'if(!card)return JSON.stringify({error:"card not found at index "+idx});' +
339
+ 'card.scrollIntoView({behavior:"smooth",block:"center"});' +
340
+ 'return JSON.stringify({success:true,scrolled:true});' +
341
+ '}catch(e){return JSON.stringify({error:e.message});}' +
342
+ '})(' + idx + ')';
343
+
344
+ const jsGetCardPositionAfterScroll = (idx) => '(function(idx){' +
345
+ 'try{' +
346
+ 'var frame=window.frames["searchFrame"];' +
347
+ 'if(!frame)return JSON.stringify({error:"searchFrame not found"});' +
348
+ 'var doc=frame.document||frame.contentDocument;' +
349
+ 'var allCards=doc.querySelectorAll("li.card-item");' +
350
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("a[data-jid][data-itemid]");}' +
351
+ 'if(allCards.length===0){allCards=doc.querySelectorAll("li");}' +
352
+ 'if(allCards.length===0)return JSON.stringify({error:"no cards found"});' +
353
+ 'var card=null;var count=0;' +
354
+ 'for(var i=0;i<allCards.length;i++){' +
355
+ 'var c=allCards[i];' +
356
+ 'if(c&&(c.dataset||c.getAttribute)){' +
357
+ 'if(count===idx){card=c;break;}' +
358
+ 'count++;}' +
359
+ '}' +
360
+ 'if(!card)return JSON.stringify({error:"card not found at index "+idx});' +
361
+ 'var rect=card.getBoundingClientRect();' +
362
+ 'var iframe=frame.frameElement;' +
363
+ 'var iframeRect=iframe?iframe.getBoundingClientRect():{left:0,top:0};' +
364
+ 'var x=iframeRect.left+rect.left+rect.width/2;' +
365
+ 'var y=iframeRect.top+rect.top+rect.height/2;' +
366
+ 'return JSON.stringify({success:true,x:Math.round(x),y:Math.round(y),width:Math.round(rect.width),height:Math.round(rect.height)});' +
367
+ '}catch(e){return JSON.stringify({error:e.message});}' +
368
+ '})(' + idx + ')';
369
+
370
+ const jsWaitForResume = `(function(){
371
+ var iframes=document.querySelectorAll('iframe');
372
+ for(var i=0;i<iframes.length;i++){
373
+ var src=iframes[i].src||'';
374
+ if(src.includes('c-resume')||src.includes('resume')||src.includes('geek')){
375
+ try{
376
+ if(iframes[i].contentDocument&&iframes[i].contentDocument.readyState==='complete'){
377
+ return JSON.stringify({found:true,state:'complete'});
378
+ }
379
+ }catch(e){}
380
+ return JSON.stringify({found:true,state:'loading'});
381
+ }
382
+ }
383
+ return JSON.stringify({found:false});
384
+ })()`;
385
+
386
+ const jsGetResumeInfo = `(function(){
387
+ var iframes=document.querySelectorAll('iframe');
388
+ for(var i=0;i<iframes.length;i++){
389
+ var src=iframes[i].src||'';
390
+ if(src.includes('c-resume')||src.includes('resume')||src.includes('geek')){
391
+ try{
392
+ var iframe=iframes[i];
393
+ var iframeDoc=iframe.contentDocument||iframe.contentWindow.document;
394
+ if(!iframeDoc||!iframeDoc.body)return JSON.stringify({error:'cannot access resume doc'});
395
+
396
+ // 优先获取内部文本内容
397
+ var bodyText=iframeDoc.body.innerText||'';
398
+ if(bodyText&&bodyText.length>50){
399
+ var info={name:'',school:'',major:'',company:'',position:'',resumeText:bodyText.substring(0,5000)};
400
+
401
+ // 尝试提取姓名
402
+ var nameEl=iframeDoc.querySelector('.name-panel .name')||iframeDoc.querySelector('.geek-top .name')||iframeDoc.querySelector('.geek-name')||iframeDoc.querySelector('[class*="name"]');
403
+ if(nameEl)info.name=nameEl.textContent.trim();
404
+
405
+ // 尝试提取学校
406
+ var schoolEl=iframeDoc.querySelector('.school-name')||iframeDoc.querySelector('.edu-school')||iframeDoc.querySelector('[class*="school"]');
407
+ if(schoolEl)info.school=schoolEl.textContent.trim();
408
+
409
+ // 尝试提取公司
410
+ var companyEl=iframeDoc.querySelector('.company-name')||iframeDoc.querySelector('.exp-company')||iframeDoc.querySelector('[class*="company"]');
411
+ if(companyEl)info.company=companyEl.textContent.trim();
412
+
413
+ return JSON.stringify(info);
414
+ }
415
+
416
+ // 如果没有文本,尝试获取innerHTML
417
+ var htmlText=iframeDoc.body.innerHTML||'';
418
+ if(htmlText){
419
+ // 去除script和style标签内容
420
+ htmlText=htmlText.replace(/<script[^>]*>[\s\S]*?<\/script>/gi,'');
421
+ htmlText=htmlText.replace(/<style[^>]*>[\s\S]*?<\/style>/gi,'');
422
+ // 获取纯文本
423
+ var temp=iframeDoc.createElement('div');
424
+ temp.innerHTML=htmlText;
425
+ var pureText=temp.textContent||temp.innerText||'';
426
+ if(pureText.length>50){
427
+ return JSON.stringify({resumeText:pureText.substring(0,5000)});
428
+ }
429
+ }
430
+
431
+ return JSON.stringify({error:'no content found'});
432
+ }catch(e){
433
+ return JSON.stringify({error:e.message});
434
+ }
435
+ }
436
+ }
437
+ return JSON.stringify({error:'resume iframe not found'});
438
+ })()`;
439
+
440
+ const jsCloseResume = `(function(){
441
+ // 使用更精确的关闭按钮选择器,与Chrome扩展一致
442
+ var closeSelectors=[
443
+ '.boss-popup__close',
444
+ '.popup-close',
445
+ '.modal-close',
446
+ '.dialog-close',
447
+ '[class*="close"]',
448
+ '.close-btn',
449
+ 'button[aria-label*="关闭"]',
450
+ 'button[title*="关闭"]',
451
+ '.icon-close'
452
+ ];
453
+
454
+ for(var i=0;i<closeSelectors.length;i++){
455
+ var closeBtns=document.querySelectorAll(closeSelectors[i]);
456
+ for(var j=0;j<closeBtns.length;j++){
457
+ var btn=closeBtns[j];
458
+ try{
459
+ // 跳过不可见的按钮
460
+ if(btn.offsetParent===null)continue;
461
+ btn.click();
462
+
463
+ // 使用更精确的modal选择器,与jsIsResumeClosed一致
464
+ var modal=btn.closest('.boss-popup__wrapper')||btn.closest('.boss-popup_wrapper')||btn.closest('.boss-dialog_wrapper')||btn.closest('.dialog-wrap')||btn.closest('.boss-dialog')||btn.closest('[class*="popup"][class*="wrapper"]')||btn.closest('[class*="dialog"][class*="wrapper"]')||btn.closest('.geek-detail-modal');
465
+ if(modal){
466
+ var style=window.getComputedStyle(modal);
467
+ if(style.display==='none'||style.visibility==='hidden'){
468
+ return JSON.stringify({success:true,method:'btn-click',selector:closeSelectors[i]});
469
+ }
470
+ } else {
471
+ // 没有找到modal容器,也认为关闭成功
472
+ return JSON.stringify({success:true,method:'btn-click',selector:closeSelectors[i]});
473
+ }
474
+ }catch(e){}
475
+ }
476
+ }
477
+
478
+ // 尝试发送ESC键关闭
479
+ var escEvent=new KeyboardEvent('keydown',{key:'Escape',code:'Escape',keyCode:27,bubbles:true});
480
+ document.dispatchEvent(escEvent);
481
+ document.body.dispatchEvent(escEvent);
482
+ return JSON.stringify({success:true,method:'ESC'});
483
+ })()`;
484
+
485
+ const jsIsResumeClosed = `(function(){
486
+ // 使用更精确的选择器,避免误判列表页上的普通元素为弹窗
487
+ // 与Chrome扩展一致:同时需要"popup/dialog"和"wrapper"两个类特征
488
+ var popupSelectors=[
489
+ '.boss-popup__wrapper',
490
+ '.boss-popup_wrapper',
491
+ '.boss-dialog_wrapper',
492
+ '.dialog-wrap.active',
493
+ '.boss-dialog',
494
+ '[class*="popup"][class*="wrapper"]',
495
+ '[class*="dialog"][class*="wrapper"]',
496
+ '.geek-detail-modal'
497
+ ];
498
+ for(var i=0;i<popupSelectors.length;i++){
499
+ try{
500
+ var popups=document.querySelectorAll(popupSelectors[i]);
501
+ for(var j=0;j<popups.length;j++){
502
+ if(popups[j].offsetParent!==null){
503
+ var style=window.getComputedStyle(popups[j]);
504
+ if(style.display!=='none'&&style.visibility!=='hidden'){
505
+ return JSON.stringify({closed:false,reason:'popup visible: '+popupSelectors[i]});
506
+ }
507
+ }
508
+ }
509
+ }catch(e){}
510
+ }
511
+
512
+ // 检查resume iframe是否可见
513
+ var iframes=document.querySelectorAll('iframe');
514
+ for(var i=0;i<iframes.length;i++){
515
+ var src=iframes[i].src||'';
516
+ if(src.includes('c-resume')||src.includes('resume')||src.includes('geek')){
517
+ try{
518
+ if(iframes[i].offsetParent!==null){
519
+ var style=window.getComputedStyle(iframes[i]);
520
+ if(style.display!=='none'&&style.visibility!=='hidden'){
521
+ return JSON.stringify({closed:false,reason:'resume iframe visible'});
522
+ }
523
+ }
524
+ }catch(e){}
525
+ }
526
+ }
527
+
528
+ return JSON.stringify({closed:true,reason:'no popup or iframe visible'});
529
+ })()`;
530
+
531
+ async function closeResumePage(cdp, maxRetries = 3) {
532
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
533
+ console.log(` 关闭详情页 (尝试 ${attempt + 1}/${maxRetries})...`);
534
+
535
+ const closeResultRaw = await cdp.send('Runtime.evaluate', { expression: jsCloseResume, returnByValue: true });
536
+ const closeResult = parseResult(closeResultRaw);
537
+
538
+ await sleep(humanDelay(500, 200));
539
+
540
+ const isClosedRaw = await cdp.send('Runtime.evaluate', { expression: jsIsResumeClosed, returnByValue: true });
541
+ const isClosed = parseResult(isClosedRaw);
542
+
543
+ if (isClosed && isClosed.closed) {
544
+ console.log(` 详情页已关闭 (${isClosed.reason})`);
545
+ return true;
546
+ }
547
+
548
+ console.log(` 详情页未关闭 (${isClosed?.reason || 'unknown'}),重试...`);
549
+ await sleep(humanDelay(500, 200));
550
+ }
551
+
552
+ console.log(' 详情页关闭失败,尝试强制关闭...');
553
+
554
+ const checkRaw = await cdp.send('Runtime.evaluate', { expression: jsIsResumeClosed, returnByValue: true });
555
+ const checkResult = parseResult(checkRaw);
556
+ if (checkResult && checkResult.closed) {
557
+ console.log(` 已确认在列表页 (${checkResult.reason})`);
558
+ return true;
559
+ }
560
+
561
+ console.log(' 详情页仍打开,发送ESC键强制关闭...');
562
+ await cdp.send('Runtime.evaluate', { expression: `(function(){
563
+ var escEvent=new KeyboardEvent('keydown',{key:'Escape',code:'Escape',keyCode:27,bubbles:true});
564
+ document.dispatchEvent(escEvent);
565
+ document.body.dispatchEvent(escEvent);
566
+ return JSON.stringify({success:true});
567
+ })()`, returnByValue: true });
568
+ await sleep(humanDelay(1000, 300));
569
+
570
+ console.log(' 再次发送ESC键...');
571
+ await cdp.send('Runtime.evaluate', { expression: `(function(){
572
+ var escEvent=new KeyboardEvent('keydown',{key:'Escape',code:'Escape',keyCode:27,bubbles:true});
573
+ document.dispatchEvent(escEvent);
574
+ document.body.dispatchEvent(escEvent);
575
+ return JSON.stringify({success:true});
576
+ })()`, returnByValue: true });
577
+ await sleep(1000);
578
+
579
+ const finalCheck = await cdp.send('Runtime.evaluate', { expression: jsIsResumeClosed, returnByValue: true });
580
+ const finalResult = parseResult(finalCheck);
581
+ if (finalResult && finalResult.closed) {
582
+ console.log(` 强制关闭成功`);
583
+ return true;
584
+ }
585
+
586
+ console.log(' 无法确认页面状态');
587
+ return false;
588
+ }
589
+
590
+ const jsGetScrollPosition = `(function(){
591
+ var frame=window.frames['searchFrame'];
592
+ if(!frame)return JSON.stringify({error:'searchFrame not found'});
593
+ var iframeDoc=frame.document||frame.contentDocument;
594
+ return JSON.stringify({
595
+ scrollTop: iframeDoc.body.scrollTop,
596
+ scrollHeight: iframeDoc.body.scrollHeight,
597
+ clientHeight: iframeDoc.body.clientHeight
598
+ });
599
+ })()`;
600
+
601
+ const jsDetectBottom = `(function(){
602
+ var frame=window.frames['searchFrame'];
603
+ if(!frame)return JSON.stringify({isBottom:false,reason:'searchFrame not found'});
604
+ var iframeDoc=frame.document||frame.contentDocument;
605
+
606
+ var bottomSelectors=['.no-more','.list-end','.end-tip','.empty-tip','.no-more-tip','.list-no-more'];
607
+ var bottomKeywords=['没有更多','已加载全部','已经到底','没有数据了','暂无更多','已显示全部'];
608
+
609
+ for(var i=0;i<bottomSelectors.length;i++){
610
+ var els=iframeDoc.querySelectorAll(bottomSelectors[i]);
611
+ for(var j=0;j<els.length;j++){
612
+ if(els[j]&&els[j].offsetParent!==null){
613
+ var text=els[j].textContent||'';
614
+ for(var k=0;k<bottomKeywords.length;k++){
615
+ if(text.indexOf(bottomKeywords[k])!==-1){
616
+ return JSON.stringify({isBottom:true,reason:'bottom text found: '+bottomKeywords[k]});
617
+ }
618
+ }
619
+ }
620
+ }
621
+ }
622
+
623
+ var divs=iframeDoc.querySelectorAll('div,span,p');
624
+ for(var i=0;i<divs.length;i++){
625
+ if(divs[i].offsetParent===null)continue;
626
+ var text=divs[i].textContent||'';
627
+ if(text.length>50)continue;
628
+ for(var k=0;k<bottomKeywords.length;k++){
629
+ if(text.indexOf(bottomKeywords[k])!==-1){
630
+ return JSON.stringify({isBottom:true,reason:'keyword found: '+bottomKeywords[k]});
631
+ }
632
+ }
633
+ }
634
+
635
+ return JSON.stringify({isBottom:false,reason:'no bottom indicator'});
636
+ })()`;
637
+
638
+ const jsScrollAndLoadMore = `(function(){
639
+ var frame=window.frames['searchFrame'];
640
+ if(!frame)return JSON.stringify({error:'searchFrame not found'});
641
+ var iframeDoc=frame.document||frame.contentDocument;
642
+
643
+ // 记录滚动前位置
644
+ var beforePos={
645
+ scrollTop: iframeDoc.body.scrollTop,
646
+ scrollHeight: iframeDoc.body.scrollHeight,
647
+ clientHeight: iframeDoc.body.clientHeight
648
+ };
649
+
650
+ // 方式1: 滚动最后一个 li 元素
651
+ var lastLi=iframeDoc.querySelector('li.geek-info-card:last-child')||iframeDoc.querySelector('li:last-child');
652
+ if(lastLi){
653
+ lastLi.scrollIntoView({behavior:'smooth',block:'end'});
654
+ }
655
+
656
+ // 方式2: 直接设置 scrollTop
657
+ var targetScroll=iframeDoc.body.scrollHeight-iframeDoc.body.clientHeight;
658
+ iframeDoc.body.scrollTop=targetScroll;
659
+
660
+ // 方式3: 使用 window.scrollTo
661
+ var win=window.frames['searchFrame'];
662
+ if(win&&win.scrollTo){
663
+ win.scrollTo(0,iframeDoc.body.scrollHeight);
664
+ }
665
+
666
+ // 触发滚动事件
667
+ var scrollEvent=new Event('scroll',{bubbles:true});
668
+ iframeDoc.body.dispatchEvent(scrollEvent);
669
+
670
+ // 记录滚动后位置
671
+ var afterPos={
672
+ scrollTop: iframeDoc.body.scrollTop,
673
+ scrollHeight: iframeDoc.body.scrollHeight,
674
+ clientHeight: iframeDoc.body.clientHeight
675
+ };
676
+
677
+ return JSON.stringify({
678
+ before: beforePos,
679
+ after: afterPos,
680
+ scrolled: beforePos.scrollTop!==afterPos.scrollTop||beforePos.scrollHeight!==afterPos.scrollHeight
681
+ });
682
+ })()`;
683
+
684
+ const jsClickFavorite = (px, py) => `(function(px,py){var iframes=document.querySelectorAll('iframe');for(var i=0;i<iframes.length;i++){var iframe=iframes[i];if(iframe.src&&iframe.src.includes('c-resume')){try{var iframeDoc=iframe.contentDocument||iframe.contentWindow.document;var canvases=iframeDoc.querySelectorAll('canvas');if(canvases.length>0){var canvas=canvases[0];var rect=canvas.getBoundingClientRect();var mousedownEvent=new MouseEvent('mousedown',{view:window,bubbles:true,cancelable:true,clientX:px,clientY:py,pageX:px,pageY:py,button:0});var mouseupEvent=new MouseEvent('mouseup',{view:window,bubbles:true,cancelable:true,clientX:px,clientY:py,pageX:px,pageY:py,button:0});var clickEvent=new MouseEvent('click',{view:window,bubbles:true,cancelable:true,clientX:px,clientY:py,pageX:px,pageY:py,button:0});canvas.dispatchEvent(mousedownEvent);canvas.dispatchEvent(mouseupEvent);canvas.dispatchEvent(clickEvent);return JSON.stringify({success:true,canvasRect:{left:rect.left,top:rect.top,width:rect.width,height:rect.height},clickPos:{x:px,y:py}});}}catch(e){return JSON.stringify({success:false,error:e.message});}break;}}return JSON.stringify({success:false,error:'Canvas not found'});})(` + px + `,` + py + `)`;
685
+
686
+ const jsGetFavoriteCanvasPosition = `(function(){
687
+ var iframes=document.querySelectorAll('iframe');
688
+ for(var i=0;i<iframes.length;i++){
689
+ var iframe=iframes[i];
690
+ if(iframe.src&&iframe.src.includes('c-resume')){
691
+ try{
692
+ var iframeDoc=iframe.contentDocument||iframe.contentWindow.document;
693
+ var canvases=iframeDoc.querySelectorAll('canvas');
694
+ if(canvases.length>0){
695
+ var canvas=canvases[0];
696
+ var canvasRect=canvas.getBoundingClientRect();
697
+ var iframeRect=iframe.getBoundingClientRect();
698
+ return JSON.stringify({
699
+ success:true,
700
+ absX:Math.round(iframeRect.left+canvasRect.left),
701
+ absY:Math.round(iframeRect.top+canvasRect.top),
702
+ width:Math.round(canvasRect.width),
703
+ height:Math.round(canvasRect.height)
704
+ });
705
+ }
706
+ }catch(e){
707
+ return JSON.stringify({success:false,error:e.message});
708
+ }
709
+ break;
710
+ }
711
+ }
712
+ return JSON.stringify({success:false,error:'Canvas not found'});
713
+ })()`;
714
+
715
+ const jsGetCardCount = `(function(){
716
+ var frame=window.frames['searchFrame'];
717
+ if(!frame)return '0';
718
+ var doc=frame.document||frame.contentDocument;
719
+
720
+ // 优先使用 li.card-item 选择器
721
+ var cards=doc.querySelectorAll('li.card-item');
722
+ if(cards.length===0){
723
+ cards=doc.querySelectorAll('a[data-jid][data-itemid]');
724
+ }
725
+ if(cards.length===0){
726
+ cards=doc.querySelectorAll('li');
727
+ }
728
+
729
+ return String(cards.length);
730
+ })()`;
731
+
732
+ async function sleep(ms) {
733
+ return new Promise(resolve => setTimeout(resolve, ms));
734
+ }
735
+
736
+ function humanDelay(baseMs, varianceMs) {
737
+ const u1 = Math.random();
738
+ const u2 = Math.random();
739
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
740
+ return Math.max(100, baseMs + z * varianceMs);
741
+ }
742
+
743
+ function generateBezierPath(start, end, steps = 20) {
744
+ const path = [];
745
+ const midX = (start.x + end.x) / 2 + (Math.random() - 0.5) * 100;
746
+ const midY = (start.y + end.y) / 2 + (Math.random() - 0.5) * 50;
747
+
748
+ for (let i = 0; i <= steps; i++) {
749
+ const t = i / steps;
750
+ const x = Math.pow(1 - t, 2) * start.x + 2 * (1 - t) * t * midX + Math.pow(t, 2) * end.x;
751
+ const y = Math.pow(1 - t, 2) * start.y + 2 * (1 - t) * t * midY + Math.pow(t, 2) * end.y;
752
+ path.push({ x, y });
753
+ }
754
+ return path;
755
+ }
756
+
757
+ async function simulateMouseMoveToArea(cdp, targetX, targetY, startX, startY) {
758
+ const actualStartX = startX || Math.round(Math.random() * 200 + 100);
759
+ const actualStartY = startY || Math.round(Math.random() * 200 + 100);
760
+ const path = generateBezierPath(
761
+ { x: actualStartX, y: actualStartY },
762
+ { x: targetX, y: targetY }
763
+ );
764
+
765
+ console.log(` [鼠标轨迹] 从 (${actualStartX}, ${actualStartY}) 移动到 (${targetX}, ${targetY}), 路径点数: ${path.length}`);
766
+
767
+ for (let i = 0; i < path.length; i++) {
768
+ const point = path[i];
769
+ const jitterX = Math.round((Math.random() - 0.5) * 3);
770
+ const jitterY = Math.round((Math.random() - 0.5) * 3);
771
+ try {
772
+ await cdp.send('Input.dispatchMouseEvent', {
773
+ type: 'mouseMoved',
774
+ x: Math.round(point.x) + jitterX,
775
+ y: Math.round(point.y) + jitterY
776
+ });
777
+ } catch (e) {}
778
+ await sleep(Math.random() * 20 + 5);
779
+ }
780
+
781
+ await sleep(humanDelay(300, 100));
782
+ console.log(` [鼠标轨迹] 移动完成,悬停中...`);
783
+ }
784
+
785
+ async function scrollDetailPage(cdp) {
786
+ console.log(` [滚动] 开始模拟阅读简历...`);
787
+
788
+ const areaX = 600 + Math.floor(Math.random() * 400);
789
+ const areaY = 300 + Math.floor(Math.random() * 300);
790
+
791
+ console.log(` [滚动] 移动鼠标到内容区域 (${areaX}, ${areaY})`);
792
+ await simulateMouseMoveToArea(cdp, areaX, areaY);
793
+
794
+ const downSteps = 2 + Math.floor(Math.random() * 2);
795
+ const downDelta = 2000 + Math.floor(Math.random() * 2000);
796
+ const totalDownDelta = downSteps * downDelta;
797
+
798
+ const upSteps = 1 + Math.floor(Math.random() * 3);
799
+ const upDelta = Math.ceil(totalDownDelta / upSteps) + 500 + Math.floor(Math.random() * 500);
800
+
801
+ console.log(` [滚动] 向下滚动 ${downSteps} 次,每次 ${downDelta}px`);
802
+
803
+ for (let i = 0; i < downSteps; i++) {
804
+ const hoverJitterX = Math.round((Math.random() - 0.5) * 6);
805
+ const hoverJitterY = Math.round((Math.random() - 0.5) * 6);
806
+ try {
807
+ await cdp.send('Input.dispatchMouseEvent', {
808
+ type: 'mouseMoved',
809
+ x: areaX + hoverJitterX,
810
+ y: areaY + hoverJitterY
811
+ });
812
+ } catch (e) {}
813
+ await sleep(100 + Math.floor(Math.random() * 200));
814
+
815
+ try {
816
+ await cdp.send('Input.dispatchMouseEvent', {
817
+ type: 'mouseWheel',
818
+ x: areaX,
819
+ y: areaY,
820
+ deltaX: 0,
821
+ deltaY: downDelta
822
+ });
823
+ console.log(` [滚动] 第 ${i + 1}/${downSteps} 次向下滚动完成`);
824
+ await sleep(200 + Math.floor(Math.random() * 300));
825
+ } catch (e) {}
826
+ }
827
+
828
+ await sleep(humanDelay(1000, 300));
829
+
830
+ console.log(` [滚动] 向上滚动 ${upSteps} 次,每次 ${upDelta}px`);
831
+
832
+ for (let i = 0; i < upSteps; i++) {
833
+ const hoverJitterX = Math.round((Math.random() - 0.5) * 6);
834
+ const hoverJitterY = Math.round((Math.random() - 0.5) * 6);
835
+ try {
836
+ await cdp.send('Input.dispatchMouseEvent', {
837
+ type: 'mouseMoved',
838
+ x: areaX + hoverJitterX,
839
+ y: areaY + hoverJitterY
840
+ });
841
+ } catch (e) {}
842
+ await sleep(100 + Math.floor(Math.random() * 200));
843
+
844
+ try {
845
+ await cdp.send('Input.dispatchMouseEvent', {
846
+ type: 'mouseWheel',
847
+ x: areaX,
848
+ y: areaY,
849
+ deltaX: 0,
850
+ deltaY: -upDelta
851
+ });
852
+ console.log(` [滚动] 第 ${i + 1}/${upSteps} 次向上滚动完成`);
853
+ await sleep(200 + Math.floor(Math.random() * 300));
854
+ } catch (e) {}
855
+ }
856
+
857
+ await sleep(humanDelay(500, 200));
858
+ console.log(` [滚动] 模拟阅读完成`);
859
+
860
+ return true;
861
+ }
862
+
863
+ async function simulateHumanClick(cdp, targetX, targetY) {
864
+ targetX = Math.round(targetX);
865
+ targetY = Math.round(targetY);
866
+
867
+ if (targetX < 0 || targetY < 0) {
868
+ throw new Error(`Invalid coordinates: (${targetX}, ${targetY})`);
869
+ }
870
+
871
+ const startPos = {
872
+ x: Math.round(Math.random() * 200 + 100),
873
+ y: Math.round(Math.random() * 200 + 100)
874
+ };
875
+
876
+ const path = generateBezierPath(startPos, { x: targetX, y: targetY });
877
+
878
+ console.log(` [鼠标轨迹] 从 (${startPos.x}, ${startPos.y}) 移动到 (${targetX}, ${targetY}), 路径点数: ${path.length}`);
879
+
880
+ for (const point of path) {
881
+ const jitterX = Math.round((Math.random() - 0.5) * 3);
882
+ const jitterY = Math.round((Math.random() - 0.5) * 3);
883
+ try {
884
+ await cdp.send('Input.dispatchMouseEvent', {
885
+ type: 'mouseMoved',
886
+ x: Math.round(point.x) + jitterX,
887
+ y: Math.round(point.y) + jitterY
888
+ });
889
+ } catch (e) {
890
+ // 忽略移动错误
891
+ }
892
+ await sleep(Math.random() * 20 + 5);
893
+ }
894
+
895
+ const hoverSteps = 3 + Math.floor(Math.random() * 5);
896
+ console.log(` [鼠标轨迹] 悬停中,抖动 ${hoverSteps} 次...`);
897
+ for (let i = 0; i < hoverSteps; i++) {
898
+ const hoverJitterX = Math.round((Math.random() - 0.5) * 6);
899
+ const hoverJitterY = Math.round((Math.random() - 0.5) * 6);
900
+ try {
901
+ await cdp.send('Input.dispatchMouseEvent', {
902
+ type: 'mouseMoved',
903
+ x: targetX + hoverJitterX,
904
+ y: targetY + hoverJitterY
905
+ });
906
+ } catch (e) {}
907
+ await sleep(Math.random() * 20 + 10);
908
+ }
909
+
910
+ const hoverDuration = humanDelay(820, 200);
911
+ console.log(` [鼠标轨迹] 悬停等待 ${Math.round(hoverDuration)}ms...`);
912
+ await sleep(hoverDuration);
913
+
914
+ try {
915
+ await cdp.send('Input.dispatchMouseEvent', {
916
+ type: 'mousePressed',
917
+ x: targetX,
918
+ y: targetY,
919
+ button: 'left',
920
+ clickCount: 1
921
+ });
922
+ console.log(` [鼠标轨迹] mousePressed`);
923
+
924
+ await sleep(Math.random() * 50 + 30);
925
+
926
+ await cdp.send('Input.dispatchMouseEvent', {
927
+ type: 'mouseReleased',
928
+ x: targetX,
929
+ y: targetY,
930
+ button: 'left',
931
+ clickCount: 1
932
+ });
933
+ console.log(` [鼠标轨迹] mouseReleased,点击完成`);
934
+ } catch (e) {
935
+ throw new Error(`CDP click failed: ${e.message}`);
936
+ }
937
+
938
+ return true;
939
+ }
940
+
941
+ function saveProgressToCsv(candidates, filepath) {
942
+ if (candidates.length === 0) {
943
+ console.log(' 没有可保存的结果');
944
+ return false;
945
+ }
946
+ try {
947
+ const header = '姓名,最高学历学校,最高学历专业,最近工作公司,最近工作职位,评估通过详细原因\n';
948
+ const rows = candidates.map(c =>
949
+ `"${c.name || ''}","${c.school || ''}","${c.major || ''}","${c.company || ''}","${c.position || ''}","${c.reason || ''}"`
950
+ ).join('\n');
951
+ fs.writeFileSync(filepath, '\ufeff' + header + rows, 'utf8');
952
+ return true;
953
+ } catch (e) {
954
+ console.log(' 保存失败:', e.message);
955
+ return false;
956
+ }
957
+ }
958
+
959
+ function setupSaveSignalHandler(passedCandidates, outputCsv) {
960
+ let saveRequested = false;
961
+
962
+ if (process.stdin.isTTY) {
963
+ readline.emitKeypressEvents(process.stdin);
964
+ process.stdin.setRawMode(true);
965
+ }
966
+
967
+ process.stdin.on('keypress', (str, key) => {
968
+ if (key.ctrl && key.name === 's') {
969
+ saveRequested = true;
970
+ } else if (key.ctrl && key.name === 'c') {
971
+ console.log('\n收到中断信号 (Ctrl+C)...');
972
+ console.log('正在保存当前进度...');
973
+ if (saveProgressToCsv(passedCandidates, outputCsv)) {
974
+ console.log(`已保存 ${passedCandidates.length} 条结果到: ${outputCsv}`);
975
+ }
976
+ process.exit(0);
977
+ }
978
+ });
979
+
980
+ return () => {
981
+ if (saveRequested) {
982
+ saveRequested = false;
983
+ console.log('\n========================================');
984
+ console.log('快速保存已触发!');
985
+ console.log(`当前已通过: ${passedCandidates.length} 人`);
986
+ if (saveProgressToCsv(passedCandidates, outputCsv)) {
987
+ console.log(`结果已保存到: ${outputCsv}`);
988
+ }
989
+ console.log('继续筛选...');
990
+ console.log('========================================');
991
+ }
992
+ };
993
+ }
994
+
995
+ function parseResult(result) {
996
+ if (result === null || result === undefined) return null;
997
+ if (typeof result === 'string') {
998
+ try {
999
+ return JSON.parse(result);
1000
+ } catch {
1001
+ return result;
1002
+ }
1003
+ }
1004
+ return result;
1005
+ }
1006
+
1007
+ function formatResumeApiData(data) {
1008
+ const parts = [];
1009
+
1010
+ const geekDetail = data.geekDetail || data;
1011
+ const baseInfo = geekDetail.geekBaseInfo || {};
1012
+ const expectList = geekDetail.geekExpectList || [];
1013
+ const workExpList = geekDetail.geekWorkExpList || [];
1014
+ const projExpList = geekDetail.geekProjExpList || [];
1015
+ const eduExpList = geekDetail.geekEduExpList || geekDetail.geekEducationList || [];
1016
+ const advantage = geekDetail.geekAdvantage || baseInfo.userDesc || baseInfo.userDescription || '';
1017
+ const skillList = geekDetail.geekSkillList || geekDetail.skillList || [];
1018
+
1019
+ parts.push('=== 基本信息===');
1020
+ if (baseInfo.name) parts.push('姓名: ' + baseInfo.name);
1021
+ if (baseInfo.ageDesc) parts.push('年龄: ' + baseInfo.ageDesc);
1022
+ if (baseInfo.gender !== undefined) parts.push('性别: ' + (baseInfo.gender === 1 ? '男' : '女'));
1023
+ if (baseInfo.degreeCategory) parts.push('学历: ' + baseInfo.degreeCategory);
1024
+ if (baseInfo.workYearDesc) parts.push('工作经验: ' + baseInfo.workYearDesc);
1025
+ if (baseInfo.activeTimeDesc) parts.push('活跃状态: ' + baseInfo.activeTimeDesc);
1026
+ if (baseInfo.applyStatusContent) parts.push('求职状态: ' + baseInfo.applyStatusContent);
1027
+
1028
+ if (expectList.length > 0) {
1029
+ parts.push('\n=== 期望工作 ===');
1030
+ expectList.forEach((expect, index) => {
1031
+ parts.push(`${index + 1}. 期望城市: ${expect.locationName || '未知'}`);
1032
+ if (expect.positionName) parts.push(' 期望职位: ' + expect.positionName);
1033
+ if (expect.salaryDesc) parts.push(' 期望薪资: ' + expect.salaryDesc);
1034
+ if (expect.industryDesc) parts.push(' 期望行业: ' + expect.industryDesc);
1035
+ });
1036
+ }
1037
+
1038
+ if (advantage) {
1039
+ parts.push('\n=== 个人优势 ===');
1040
+ parts.push(advantage.replace(/<em class='h'>/g, '').replace(/<\/em>/g, ''));
1041
+ }
1042
+
1043
+ if (workExpList.length > 0) {
1044
+ parts.push('\n=== 工作经历 ===');
1045
+ workExpList.forEach((exp, index) => {
1046
+ const company = exp.company || '';
1047
+ const position = (exp.positionName || '').replace(/<em class='h'>/g, '').replace(/<\/em>/g, '');
1048
+ parts.push(`${index + 1}. ${company} - ${position}`);
1049
+ if (exp.startYearMonStr) {
1050
+ parts.push(' 时间: ' + exp.startYearMonStr + ' ~ ' + (exp.endYearMonStr || '至今'));
1051
+ }
1052
+ if (exp.responsibility) {
1053
+ const responsibility = exp.responsibility.replace(/<em class='h'>/g, '').replace(/<\/em>/g, '');
1054
+ parts.push(' 职责: ' + responsibility);
1055
+ }
1056
+ });
1057
+ }
1058
+
1059
+ if (projExpList.length > 0) {
1060
+ parts.push('\n=== 项目经历 ===');
1061
+ projExpList.forEach((proj, index) => {
1062
+ parts.push(`${index + 1}. ${proj.name || '未知项目'}`);
1063
+ if (proj.roleName) parts.push(' 角色: ' + proj.roleName);
1064
+ if (proj.startYearMonStr) {
1065
+ parts.push(' 时间: ' + proj.startYearMonStr + ' ~ ' + (proj.endYearMonStr || '至今'));
1066
+ }
1067
+ if (proj.description) {
1068
+ const description = proj.description.replace(/<em class='h'>/g, '').replace(/<\/em>/g, '');
1069
+ parts.push(' 描述: ' + description);
1070
+ }
1071
+ if (proj.performance) {
1072
+ const performance = proj.performance.replace(/<em class='h'>/g, '').replace(/<\/em>/g, '');
1073
+ parts.push(' 成果: ' + performance);
1074
+ }
1075
+ });
1076
+ }
1077
+
1078
+ if (eduExpList.length > 0) {
1079
+ parts.push('\n=== 教育经历 ===');
1080
+ eduExpList.forEach((edu, index) => {
1081
+ parts.push(`${index + 1}. ${edu.school || edu.schoolName || '未知学校'}`);
1082
+ if (edu.major || edu.majorName) parts.push(' 专业: ' + (edu.major || edu.majorName));
1083
+ if (edu.degree || edu.degreeCategory) parts.push(' 学历: ' + (edu.degree || edu.degreeCategory));
1084
+ if (edu.startYearMonStr) {
1085
+ parts.push(' 时间: ' + edu.startYearMonStr + ' ~ ' + (edu.endYearMonStr || ''));
1086
+ }
1087
+ });
1088
+ }
1089
+
1090
+ if (skillList.length > 0) {
1091
+ parts.push('\n=== 技能标签 ===');
1092
+ skillList.forEach((skill) => {
1093
+ if (skill.skillName || skill.name) {
1094
+ parts.push('- ' + (skill.skillName || skill.name) + (skill.level ? ' (' + skill.level + ')' : ''));
1095
+ }
1096
+ });
1097
+ }
1098
+
1099
+ return parts.join('\n');
1100
+ }
1101
+
1102
+ async function callLLM(prompt, maxRetries = 3) {
1103
+ const strictPrompt = `【重要】你必须且只能返回以下格式的JSON,禁止返回任何其他文字、解释或格式:
1104
+
1105
+ {"passed":true/false,"reason":"通过/不通过的具体原因","summary":"简历摘要"}
1106
+
1107
+ 【示例响应】
1108
+ {"passed":true,"reason":"硕士学历,符合本科及以上要求","summary":"厦门大学硕士,研究方向匹配"}
1109
+
1110
+ 请直接返回JSON,不要有任何前缀或后缀文字:
1111
+
1112
+ ${prompt}`;
1113
+
1114
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1115
+ const response = await fetch(`${baseUrl}/chat/completions`, {
1116
+ method: 'POST',
1117
+ headers: {
1118
+ 'Content-Type': 'application/json',
1119
+ 'Authorization': `Bearer ${apiKey}`
1120
+ },
1121
+ body: JSON.stringify({
1122
+ model: model,
1123
+ messages: [{ role: 'user', content: strictPrompt }],
1124
+ temperature: 0.1,
1125
+ max_tokens: 500
1126
+ })
1127
+ });
1128
+
1129
+ if (!response.ok) {
1130
+ const errorText = await response.text();
1131
+ throw new Error(`API请求失败: ${response.status} ${response.statusText} - ${errorText}`);
1132
+ }
1133
+
1134
+ const data = await response.json();
1135
+
1136
+ if (data.error) {
1137
+ throw new Error(`API错误: ${data.error.message}`);
1138
+ }
1139
+
1140
+ const content = data.choices[0].message.content;
1141
+
1142
+ try {
1143
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
1144
+ if (jsonMatch) {
1145
+ const result = JSON.parse(jsonMatch[0]);
1146
+ if (typeof result.passed === 'boolean' && result.reason && result.summary) {
1147
+ return content;
1148
+ }
1149
+ }
1150
+ if (attempt < maxRetries - 1) {
1151
+ console.log(' LLM返回格式不规范,重试...');
1152
+ continue;
1153
+ }
1154
+ } catch (e) {
1155
+ if (attempt < maxRetries - 1) {
1156
+ console.log(' JSON解析失败,重试...');
1157
+ continue;
1158
+ }
1159
+ }
1160
+
1161
+ return content;
1162
+ }
1163
+
1164
+ throw new Error('LLM返回格式不规范,已达最大重试次数');
1165
+ }
1166
+
1167
+ async function main() {
1168
+ console.log('========================================');
1169
+ console.log('BOSS直聘简历筛选CLI工具 (Node.js)');
1170
+ console.log('========================================');
1171
+ console.log('');
1172
+ console.log('配置:');
1173
+ console.log(` 目标人数: ${targetCount}`);
1174
+ console.log(` 模型: ${model}`);
1175
+ console.log(` 筛选标准: ${criteria}`);
1176
+ console.log('');
1177
+
1178
+ const pos = loadCalibration();
1179
+ console.log(`已加载校准坐标: pageX=${pos.pageX}, pageY=${pos.pageY}`);
1180
+ console.log('');
1181
+
1182
+ console.log('[1/6] 连接Chrome...');
1183
+ const tab = await getChromeTab();
1184
+ console.log(`找到: ${tab.title}`);
1185
+
1186
+ const cdp = new CDPClient(tab.webSocketDebuggerUrl);
1187
+ await new Promise(r => setTimeout(r, 500));
1188
+ console.log('WebSocket已连接!');
1189
+
1190
+ console.log('[1.5/6] 启用网络拦截...');
1191
+ await enableNetworkInterception(cdp);
1192
+ console.log('网络拦截已启用!');
1193
+ console.log('');
1194
+
1195
+ console.log('[2/6] 检查当前页面状态...');
1196
+ const currentUrl = await cdp.send('Runtime.evaluate', { expression: 'window.location.href', returnByValue: true });
1197
+ console.log(`当前页面: ${currentUrl}`);
1198
+ console.log('');
1199
+
1200
+ console.log('[3/6] 获取列表页信息...');
1201
+ const listInfoRaw = await cdp.send('Runtime.evaluate', { expression: jsGetList, returnByValue: true });
1202
+
1203
+ const listInfo = parseResult(listInfoRaw);
1204
+
1205
+ if (!listInfo || listInfo.error) {
1206
+ console.error(`错误: ${listInfo ? listInfo.error : 'CDP timeout'}`);
1207
+ cdp.close();
1208
+ process.exit(1);
1209
+ }
1210
+ console.log(`当前列表页显示: ${listInfo.totalCards} 个人选`);
1211
+ console.log('');
1212
+
1213
+ console.log('[4/6] 开始筛选流程...');
1214
+ console.log('========================================');
1215
+ console.log('快捷键: Ctrl+S 保存当前进度 | Ctrl+C 保存并退出');
1216
+ console.log('========================================');
1217
+ console.log('');
1218
+
1219
+ const passedCandidates = [];
1220
+ let processedCount = 0;
1221
+ let currentCardIndex = 0;
1222
+ let lastCardCount = listInfo.totalCards;
1223
+ let scrollRetryCount = 0;
1224
+ const maxScrollRetries = 3;
1225
+ const processedCardKeys = new Set();
1226
+ let consecutiveCount = 0;
1227
+ let restThreshold = 30 + Math.floor(Math.random() * 11);
1228
+ let uncertainFavoriteCount = 0;
1229
+
1230
+ const checkAndHandleSave = setupSaveSignalHandler(passedCandidates, outputCsv);
1231
+
1232
+ while (processedCount < targetCount) {
1233
+ console.log('');
1234
+ console.log('----------------------------------------');
1235
+ console.log(`处理进度: ${processedCount}/${targetCount} 已通过 ${passedCandidates.length} 人 未确认收藏 ${uncertainFavoriteCount} 人`);
1236
+
1237
+ const processedKeysArray = Array.from(processedCardKeys);
1238
+ const findCardExpr = jsFindNextUnprocessedCard + '(' + currentCardIndex + ',' + JSON.stringify(processedKeysArray) + ')';
1239
+ const nextCardRaw = await cdp.send('Runtime.evaluate', { expression: findCardExpr, returnByValue: true });
1240
+ const nextCard = parseResult(nextCardRaw);
1241
+
1242
+ checkAndHandleSave();
1243
+
1244
+
1245
+ if (!nextCard || !nextCard.found) {
1246
+ console.log('列表已到底,尝试滚动加载更多...');
1247
+
1248
+ const scrollBeforeRaw = await cdp.send('Runtime.evaluate', { expression: jsGetScrollPosition, returnByValue: true });
1249
+ const scrollBefore = parseResult(scrollBeforeRaw);
1250
+
1251
+ const scrollResultRaw = await cdp.send('Runtime.evaluate', { expression: jsScrollAndLoadMore, returnByValue: true });
1252
+ const scrollResult = parseResult(scrollResultRaw);
1253
+
1254
+ await sleep(humanDelay(1500, 500));
1255
+
1256
+ const scrollAfterRaw = await cdp.send('Runtime.evaluate', { expression: jsGetScrollPosition, returnByValue: true });
1257
+ const scrollAfter = parseResult(scrollAfterRaw);
1258
+
1259
+ const bottomResultRaw = await cdp.send('Runtime.evaluate', { expression: jsDetectBottom, returnByValue: true });
1260
+ const bottomResult = parseResult(bottomResultRaw);
1261
+
1262
+ const newCountRaw = await cdp.send('Runtime.evaluate', { expression: jsGetCardCount, returnByValue: true });
1263
+ const newCount = parseResult(newCountRaw);
1264
+ const actualNewCount = typeof newCount === 'string' ? parseInt(newCount, 10) : (typeof newCount === 'number' ? newCount : 0);
1265
+
1266
+ const didScroll = scrollBefore && scrollAfter &&
1267
+ (scrollBefore.scrollTop !== scrollAfter.scrollTop ||
1268
+ scrollBefore.scrollHeight !== scrollAfter.scrollHeight);
1269
+
1270
+ if (bottomResult && bottomResult.isBottom) {
1271
+ console.log(`检测到底部提示: ${bottomResult.reason}`);
1272
+ console.log('已到达列表底部,结束筛选');
1273
+ break;
1274
+ }
1275
+
1276
+ if (actualNewCount > lastCardCount) {
1277
+ lastCardCount = actualNewCount;
1278
+ scrollRetryCount = 0;
1279
+ console.log(`加载成功,当前共 ${lastCardCount} 个人选`);
1280
+ continue;
1281
+ }
1282
+
1283
+ if (!didScroll) {
1284
+ console.log('滚动未生效,重试...');
1285
+ scrollRetryCount++;
1286
+ if (scrollRetryCount >= maxScrollRetries) {
1287
+ console.log('滚动多次未生效,结束筛选');
1288
+ break;
1289
+ }
1290
+ continue;
1291
+ }
1292
+
1293
+ scrollRetryCount++;
1294
+ if (scrollRetryCount >= maxScrollRetries) {
1295
+ console.log('已无法加载更多候选人,结束筛选');
1296
+ break;
1297
+ }
1298
+ console.log(`滚动后数量未增加,重试 (${scrollRetryCount}/${maxScrollRetries})...`);
1299
+ continue;
1300
+ }
1301
+
1302
+ processedCount++;
1303
+ const cardKey = nextCard.key || nextCard.jid || ('item_' + nextCard.index);
1304
+ processedCardKeys.add(cardKey);
1305
+ currentCardIndex = nextCard.index + 1;
1306
+ console.log('');
1307
+ console.log(`>>> 点击第 ${nextCard.index + 1} 位人选 (key: ${cardKey})`);
1308
+
1309
+ const scrollResultRaw = await cdp.send('Runtime.evaluate', { expression: jsGetCardPosition(nextCard.index), returnByValue: true });
1310
+ const scrollResult = parseResult(scrollResultRaw);
1311
+
1312
+ if (scrollResult && scrollResult.success && scrollResult.scrolled) {
1313
+ console.log(` 滚动到卡片位置 (center)`);
1314
+ await sleep(humanDelay(500, 200));
1315
+ }
1316
+
1317
+ const cardPosRaw = await cdp.send('Runtime.evaluate', { expression: jsGetCardPositionAfterScroll(nextCard.index), returnByValue: true });
1318
+ const cardPos = parseResult(cardPosRaw);
1319
+
1320
+ if (cardPos && cardPos.success && cardPos.x && cardPos.y) {
1321
+ console.log(` 卡片坐标: (${cardPos.x}, ${cardPos.y}), 尺寸: ${cardPos.width}x${cardPos.height}`);
1322
+ const offsetX = Math.floor(Math.random() * 60) - 30;
1323
+ const maxOffsetY = Math.min(50, Math.floor(cardPos.height / 3));
1324
+ const offsetY = Math.floor(Math.random() * maxOffsetY * 2) - maxOffsetY;
1325
+ const clickX = cardPos.x + offsetX;
1326
+ const clickY = cardPos.y + offsetY;
1327
+ console.log(` 使用CDP鼠标轨迹点击 (${clickX}, ${clickY}) [偏移: ${offsetX}, ${offsetY}]...`);
1328
+ try {
1329
+ await simulateHumanClick(cdp, clickX, clickY);
1330
+ console.log(' 鼠标轨迹点击完成');
1331
+ } catch (e) {
1332
+ console.log(` CDP点击失败: ${e.message},回退到JS点击`);
1333
+ const clickResultRaw = await cdp.send('Runtime.evaluate', { expression: jsClickCard(nextCard.index), returnByValue: true });
1334
+ const clickResult = parseResult(clickResultRaw);
1335
+ if (!clickResult || !clickResult.success) {
1336
+ console.log(` JS点击也失败: ${clickResult ? clickResult.error : 'CDP timeout'}`);
1337
+ continue;
1338
+ }
1339
+ }
1340
+ } else {
1341
+ console.log(` 无法获取卡片坐标,使用JS点击: ${cardPos?.error || 'unknown'}`);
1342
+ const clickResultRaw = await cdp.send('Runtime.evaluate', { expression: jsClickCard(nextCard.index), returnByValue: true });
1343
+ const clickResult = parseResult(clickResultRaw);
1344
+ if (!clickResult || !clickResult.success) {
1345
+ console.log(` 点击失败: ${clickResult ? clickResult.error : 'CDP timeout'}`);
1346
+ continue;
1347
+ }
1348
+ }
1349
+
1350
+ console.log(' 等待详情页加载...');
1351
+ let detailLoaded = false;
1352
+ for (let i = 0; i < 20; i++) {
1353
+ await sleep(humanDelay(500, 150));
1354
+ const hasResumeRaw = await cdp.send('Runtime.evaluate', { expression: jsWaitForResume, returnByValue: true });
1355
+ const hasResume = parseResult(hasResumeRaw);
1356
+ if (hasResume && hasResume.found) {
1357
+ detailLoaded = true;
1358
+ console.log(` 详情页已加载 (状态: ${hasResume.state || 'unknown'})`);
1359
+ break;
1360
+ }
1361
+ }
1362
+
1363
+ if (!detailLoaded) {
1364
+ console.log(' 详情页加载超时,跳过');
1365
+ continue;
1366
+ }
1367
+ console.log(' 详情页已加载!');
1368
+
1369
+ console.log(' 等待简历API响应...');
1370
+ capturedResumeData = null;
1371
+ let apiResumeData = null;
1372
+
1373
+ await sleep(humanDelay(1000, 300));
1374
+ if (resumeRequestId) {
1375
+ console.log(' 尝试直接获取简历数据...');
1376
+ apiResumeData = await getResumeDataViaCDP(cdp, resumeRequestId);
1377
+ if (apiResumeData) {
1378
+ capturedResumeData = apiResumeData;
1379
+ console.log(` 通过CDP直接获取到简历: ${apiResumeData.geekDetail?.geekBaseInfo?.name || '未知'}`);
1380
+ }
1381
+ }
1382
+
1383
+ if (!capturedResumeData) {
1384
+ for (let wait = 0; wait < 8; wait++) {
1385
+ await sleep(humanDelay(500, 150));
1386
+ if (capturedResumeData) {
1387
+ apiResumeData = capturedResumeData;
1388
+ console.log(` 通过回调获取到简历: ${apiResumeData.geekDetail?.geekBaseInfo?.name || '未知'}`);
1389
+ break;
1390
+ }
1391
+ }
1392
+ }
1393
+
1394
+ let candidateInfo = null;
1395
+ let resumeData = null;
1396
+ if (apiResumeData || capturedResumeData) {
1397
+ resumeData = apiResumeData || capturedResumeData;
1398
+ const geekDetail = resumeData.geekDetail || resumeData;
1399
+ const baseInfo = geekDetail.geekBaseInfo || {};
1400
+ candidateInfo = {
1401
+ name: baseInfo.name || geekDetail.geekName || resumeData.geekName || '',
1402
+ school: (geekDetail.geekEduExpList && geekDetail.geekEduExpList[0]?.school) || (geekDetail.geekEducationList && geekDetail.geekEducationList[0]?.school) || '',
1403
+ major: (geekDetail.geekEduExpList && geekDetail.geekEduExpList[0]?.major) || (geekDetail.geekEducationList && geekDetail.geekEducationList[0]?.major) || '',
1404
+ company: (geekDetail.geekWorkExpList && geekDetail.geekWorkExpList[0]?.company) || '',
1405
+ position: (geekDetail.geekWorkExpList && geekDetail.geekWorkExpList[0]?.positionName) || '',
1406
+ resumeText: formatResumeApiData(resumeData),
1407
+ alreadyInterested: resumeData.alreadyInterested === true
1408
+ };
1409
+ } else {
1410
+ console.log(' API未返回数据,尝试从DOM提取...');
1411
+ const candidateInfoRaw = await cdp.send('Runtime.evaluate', { expression: jsGetResumeInfo, returnByValue: true });
1412
+ candidateInfo = parseResult(candidateInfoRaw);
1413
+ }
1414
+
1415
+ if (!candidateInfo || candidateInfo.error || !candidateInfo.resumeText) {
1416
+ console.log(` 获取简历信息失败`);
1417
+ await closeResumePage(cdp);
1418
+ await sleep(humanDelay(800, 200));
1419
+ continue;
1420
+ }
1421
+
1422
+ console.log(` 姓名: ${candidateInfo.name || '未知'}`);
1423
+ console.log(` 学校: ${candidateInfo.school || '未知'}`);
1424
+ console.log(` 公司: ${candidateInfo.company || '未知'}`);
1425
+
1426
+ console.log(' 调用LLM评估...');
1427
+ const prompt = `你是一位专业的HR招聘助手,请根据以下筛选标准分析候选人简历,判断是否匹配。\n\n筛选标准:\n${criteria}\n\n简历内容:\n${candidateInfo.resumeText}\n\n请仔细分析简历,返回以下格式的JSON(必须是有效的JSON格式,不要包含任何其他内容):\n{\n "passed": true或false,\n "reason": "通过或不通过的具体原因",\n "summary": "简历摘要"\n}`;
1428
+
1429
+ try {
1430
+ const content = await callLLM(prompt);
1431
+
1432
+ let passed = false;
1433
+ let reason = '';
1434
+ let summary = '';
1435
+
1436
+ console.log(` LLM返回内容: ${content.substring(0, 100)}...`);
1437
+
1438
+ try {
1439
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
1440
+ if (jsonMatch) {
1441
+ const result = JSON.parse(jsonMatch[0]);
1442
+ passed = result.passed;
1443
+ reason = result.reason || '';
1444
+ summary = result.summary || '';
1445
+ } else {
1446
+ console.log(' LLM返回不是JSON格式');
1447
+ }
1448
+ } catch (jsonError) {
1449
+ console.log(` JSON解析失败: ${jsonError.message}`);
1450
+ console.log(' 跳过此人选');
1451
+ await closeResumePage(cdp);
1452
+ await sleep(humanDelay(800, 200));
1453
+ continue;
1454
+ }
1455
+
1456
+ console.log(' 模拟阅读简历(滚动详情页)...');
1457
+ const scrolled = await scrollDetailPage(cdp);
1458
+ if (scrolled) {
1459
+ console.log(' 详情页滚动完成');
1460
+ } else {
1461
+ console.log(' 详情页无需滚动或滚动失败');
1462
+ }
1463
+
1464
+ if (passed) {
1465
+ console.log(' LLM评估结果: 通过');
1466
+ console.log(` 原因: ${reason}`);
1467
+
1468
+ console.log(' 执行收藏操作...');
1469
+ let favoriteDone = false;
1470
+ let clickCount = 0;
1471
+ const maxClicks = 5;
1472
+
1473
+ while (clickCount < maxClicks && !favoriteDone) {
1474
+ clickCount++;
1475
+ favoriteActionResult = null;
1476
+ pendingFavoriteClick = true;
1477
+
1478
+ try {
1479
+ await cdp.send('Page.bringToFront');
1480
+ } catch (e) {}
1481
+
1482
+ await sleep(humanDelay(200, 100));
1483
+
1484
+ const canvasPosRaw = await cdp.send('Runtime.evaluate', { expression: jsGetFavoriteCanvasPosition, returnByValue: true });
1485
+ const canvasPos = parseResult(canvasPosRaw);
1486
+
1487
+ if (canvasPos && canvasPos.success) {
1488
+ const offsetX = Math.floor(Math.random() * 7) - 3;
1489
+ const offsetY = Math.floor(Math.random() * 7) - 3;
1490
+ const clickX = canvasPos.absX + pos.canvasX + offsetX;
1491
+ const clickY = canvasPos.absY + pos.canvasY + offsetY;
1492
+
1493
+ console.log(` 使用CDP鼠标轨迹点击收藏按钮 (${clickX}, ${clickY})...`);
1494
+
1495
+ try {
1496
+ await simulateHumanClick(cdp, clickX, clickY);
1497
+ console.log(' CDP点击完成');
1498
+ } catch (e) {
1499
+ console.log(` CDP点击失败: ${e.message},回退到JS点击`);
1500
+ const favResultRaw = await cdp.send('Runtime.evaluate', { expression: jsClickFavorite(pos.pageX + offsetX, pos.pageY + offsetY), returnByValue: true });
1501
+ const favResult = parseResult(favResultRaw);
1502
+ if (!favResult || !favResult.success) {
1503
+ console.log(` JS点击也失败: ${favResult?.error || 'unknown'}`);
1504
+ pendingFavoriteClick = false;
1505
+ break;
1506
+ }
1507
+ }
1508
+ } else {
1509
+ console.log(` 无法获取Canvas位置,使用校准坐标: ${canvasPos?.error || 'unknown'}`);
1510
+ const offsetX = Math.floor(Math.random() * 7) - 3;
1511
+ const offsetY = Math.floor(Math.random() * 7) - 3;
1512
+ const clickX = pos.pageX + offsetX;
1513
+ const clickY = pos.pageY + offsetY;
1514
+
1515
+ try {
1516
+ await simulateHumanClick(cdp, clickX, clickY);
1517
+ console.log(' CDP点击完成');
1518
+ } catch (e) {
1519
+ console.log(` CDP点击失败: ${e.message},回退到JS点击`);
1520
+ const favResultRaw = await cdp.send('Runtime.evaluate', { expression: jsClickFavorite(clickX, clickY), returnByValue: true });
1521
+ const favResult = parseResult(favResultRaw);
1522
+ if (!favResult || !favResult.success) {
1523
+ console.log(` JS点击也失败: ${favResult?.error || 'unknown'}`);
1524
+ pendingFavoriteClick = false;
1525
+ break;
1526
+ }
1527
+ }
1528
+ }
1529
+
1530
+ let waitResult = null;
1531
+ for (let wait = 0; wait < 5; wait++) {
1532
+ await sleep(humanDelay(500, 150));
1533
+ if (favoriteActionResult) {
1534
+ waitResult = favoriteActionResult;
1535
+ break;
1536
+ }
1537
+ }
1538
+
1539
+ if (waitResult === 'add') {
1540
+ console.log(` 收藏成功`);
1541
+ favoriteDone = true;
1542
+ } else if (waitResult === 'del') {
1543
+ console.log(` 检测到取消收藏,重新点击...`);
1544
+ } else {
1545
+ if (clickCount < maxClicks) {
1546
+ console.log(` 第${clickCount}次未检测到响应,重试...`);
1547
+ }
1548
+ }
1549
+ }
1550
+
1551
+ if (!favoriteDone) {
1552
+ console.log(' 收藏操作未能确认成功');
1553
+ uncertainFavoriteCount++;
1554
+ }
1555
+
1556
+ pendingFavoriteClick = false;
1557
+
1558
+ passedCandidates.push({
1559
+ name: candidateInfo.name,
1560
+ school: candidateInfo.school,
1561
+ major: candidateInfo.major,
1562
+ company: candidateInfo.company,
1563
+ position: candidateInfo.position,
1564
+ reason: reason,
1565
+ summary: summary
1566
+ });
1567
+ } else {
1568
+ console.log(' LLM评估结果: 不通过');
1569
+ console.log(` 原因: ${reason}`);
1570
+ }
1571
+ } catch (e) {
1572
+ console.log(` LLM调用失败: ${e.message}`);
1573
+ }
1574
+
1575
+ await closeResumePage(cdp);
1576
+ await sleep(humanDelay(800, 200));
1577
+
1578
+ consecutiveCount++;
1579
+
1580
+ if (Math.random() < 0.1) {
1581
+ const shortBreak = 5000 + Math.random() * 10000;
1582
+ console.log('');
1583
+ console.log(`[随机休息] 10%概率触发,休息 ${Math.round(shortBreak/1000)} 秒...`);
1584
+ for (let i = 0; i < shortBreak; i += 1000) {
1585
+ checkAndHandleSave();
1586
+ await sleep(1000);
1587
+ }
1588
+ console.log('随机休息结束,继续筛选...');
1589
+ }
1590
+
1591
+ if (consecutiveCount >= restThreshold) {
1592
+ const breakTime = 120000 + Math.random() * 180000;
1593
+ const breakMinutes = Math.round(breakTime / 60000);
1594
+ console.log('');
1595
+ console.log(`========================================`);
1596
+ console.log(`已连续处理 ${consecutiveCount} 人,随机休息 ${breakMinutes} 分钟...`);
1597
+ console.log(`========================================`);
1598
+ for (let i = 0; i < breakTime; i += 1000) {
1599
+ checkAndHandleSave();
1600
+ await sleep(1000);
1601
+ }
1602
+ consecutiveCount = 0;
1603
+ restThreshold = 30 + Math.floor(Math.random() * 11);
1604
+ console.log('休息结束,继续筛选...');
1605
+ }
1606
+
1607
+ if (processedCount >= targetCount * 3) {
1608
+ console.log('警告: 已处理超过目标数量3倍,强制结束');
1609
+ break;
1610
+ }
1611
+ }
1612
+
1613
+ console.log('');
1614
+ console.log('[5/6] 导出结果到CSV...');
1615
+
1616
+ if (passedCandidates.length > 0) {
1617
+ const header = '姓名,最高学历学校,最高学历专业,最近工作公司,最近工作职位,评估通过详细原因\n';
1618
+ const rows = passedCandidates.map(c =>
1619
+ `"${c.name}","${c.school}","${c.major}","${c.company}","${c.position}","${c.reason}"`
1620
+ ).join('\n');
1621
+ fs.writeFileSync(outputCsv, '\ufeff' + header + rows, 'utf8');
1622
+ console.log(`结果已导出到: ${outputCsv}`);
1623
+ console.log(`共导出 ${passedCandidates.length} 位通过筛选的人选`);
1624
+ } else {
1625
+ console.log('没有通过筛选的人选');
1626
+ }
1627
+
1628
+ console.log('[6/6] 清理资源...');
1629
+ cdp.close();
1630
+
1631
+ console.log('');
1632
+ console.log('========================================');
1633
+ console.log('筛选完成!');
1634
+ console.log('========================================');
1635
+ console.log('处理结果:');
1636
+ console.log(` 已处理: ${processedCount} 人`);
1637
+ console.log(` 通过筛选: ${passedCandidates.length} 人`);
1638
+ console.log(` 目标人数: ${targetCount} 人`);
1639
+ console.log('');
1640
+ console.log('Done.');
1641
+ }
1642
+
1643
+ main().catch(e => {
1644
+ console.error('Fatal error:', e);
1645
+ process.exit(1);
1646
+ });