@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.24

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.
Files changed (66) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/core/boss-cards/index.js +199 -199
  14. package/src/core/browser/index.js +286 -136
  15. package/src/core/capture/index.js +2930 -1201
  16. package/src/core/cv-acquisition/index.js +512 -238
  17. package/src/core/cv-capture-target/index.js +513 -299
  18. package/src/core/greet-quota/index.js +71 -71
  19. package/src/core/infinite-list/index.js +31 -31
  20. package/src/core/reporting/legacy-csv.js +12 -12
  21. package/src/core/run/detached-launcher.js +305 -0
  22. package/src/core/run/index.js +105 -52
  23. package/src/core/run/timing.js +33 -33
  24. package/src/core/run/windows-detached-worker.ps1 +106 -0
  25. package/src/core/screening/index.js +2135 -2135
  26. package/src/core/self-heal/index.js +989 -973
  27. package/src/core/self-heal/viewport.js +1505 -564
  28. package/src/detached-worker.js +99 -99
  29. package/src/domains/chat/action-journal.js +443 -0
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +1684 -401
  33. package/src/domains/chat/index.js +8 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +157 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +1801 -760
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +515 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +92 -92
  44. package/src/domains/recommend/detail.js +12 -12
  45. package/src/domains/recommend/filters.js +611 -611
  46. package/src/domains/recommend/index.js +9 -9
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -736
  49. package/src/domains/recommend/refresh.js +422 -422
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1791 -1205
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1817 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +16 -1
  64. package/src/parser.js +1296 -1296
  65. package/src/recommend-mcp.js +551 -362
  66. package/src/recommend-scheduler.js +75 -75
@@ -1,588 +1,588 @@
1
- import {
2
- clickNodeCenter,
3
- clickPoint,
4
- DETERMINISTIC_CLICK_OPTIONS,
5
- getFrameDocumentNodeId,
6
- getNodeBox,
7
- getOuterHTML,
8
- pressKey,
9
- querySelectorAll,
10
- scrollNodeIntoView,
11
- sleep
12
- } from "../../core/browser/index.js";
13
- import {
14
- buildScreeningCandidateFromDetail,
15
- htmlToText
16
- } from "../../core/screening/index.js";
17
- import {
18
- closeBossAccountRightsBlockingPanel,
19
- findBossAccountRightsBlockingPanel
20
- } from "../common/account-rights-panel.js";
21
- import {
22
- RECRUIT_DETAIL_CLOSE_SELECTORS,
23
- RECRUIT_DETAIL_NETWORK_PATTERNS,
24
- RECRUIT_DETAIL_POPUP_SELECTORS,
25
- RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS
26
- } from "./constants.js";
27
- import {
28
- getRecruitRoots
29
- } from "./roots.js";
30
-
31
- function compactBox(box = null) {
32
- if (!box) return null;
33
- return {
34
- center: box.center || null,
35
- rect: box.rect || null
36
- };
37
- }
38
-
39
- async function getViewportRect(client) {
40
- if (typeof client?.Page?.getLayoutMetrics !== "function") return null;
41
- try {
42
- const metrics = await client.Page.getLayoutMetrics();
43
- const viewport = metrics?.cssVisualViewport || metrics?.visualViewport || metrics?.layoutViewport || {};
44
- const width = Number(viewport.clientWidth || viewport.width || metrics?.layoutViewport?.clientWidth || 0);
45
- const height = Number(viewport.clientHeight || viewport.height || metrics?.layoutViewport?.clientHeight || 0);
46
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
47
- return {
48
- x: 0,
49
- y: 0,
50
- width,
51
- height
52
- };
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- function boxCenterIsInViewport(box, viewport, { marginPx = 16 } = {}) {
59
- if (!box?.center || !viewport) return null;
60
- const margin = Math.max(0, Number(marginPx) || 0);
61
- return (
62
- box.center.x >= viewport.x + margin
63
- && box.center.x <= viewport.x + viewport.width - margin
64
- && box.center.y >= viewport.y + margin
65
- && box.center.y <= viewport.y + viewport.height - margin
66
- );
67
- }
68
-
69
- function scrollDeltaForBox(box, viewport) {
70
- if (!box?.center || !viewport) return 0;
71
- const targetY = viewport.y + viewport.height * 0.48;
72
- const delta = box.center.y - targetY;
73
- if (Math.abs(delta) < 80) return 0;
74
- return Math.max(-900, Math.min(900, delta));
75
- }
76
-
77
- export async function ensureRecruitCardInViewport(client, cardNodeId, {
78
- maxScrollAttempts = 4,
79
- marginPx = 16,
80
- settleMs = 220
81
- } = {}) {
82
- const attempts = [];
83
- await scrollNodeIntoView(client, cardNodeId);
84
- if (settleMs > 0) await sleep(settleMs);
85
-
86
- let finalBox = null;
87
- for (let attempt = 0; attempt <= maxScrollAttempts; attempt += 1) {
88
- const box = await getNodeBox(client, cardNodeId);
89
- finalBox = box;
90
- const viewport = await getViewportRect(client);
91
- const inViewport = boxCenterIsInViewport(box, viewport, { marginPx });
92
- const entry = {
93
- attempt,
94
- in_viewport: inViewport,
95
- viewport,
96
- box: compactBox(box)
97
- };
98
- attempts.push(entry);
99
- if (inViewport === true || inViewport === null) {
100
- return {
101
- ok: inViewport !== false,
102
- verified: inViewport === true,
103
- box,
104
- attempts
105
- };
106
- }
107
- if (attempt >= maxScrollAttempts) break;
108
-
109
- const deltaY = scrollDeltaForBox(box, viewport);
110
- if (!deltaY) break;
111
- const wheelPoint = {
112
- x: viewport.x + viewport.width * 0.5,
113
- y: viewport.y + viewport.height * 0.5
114
- };
115
- await client.Input.dispatchMouseEvent({
116
- type: "mouseWheel",
117
- x: wheelPoint.x,
118
- y: wheelPoint.y,
119
- deltaX: 0,
120
- deltaY
121
- });
122
- entry.scroll = {
123
- method: "mouseWheel",
124
- delta_y: deltaY,
125
- point: wheelPoint
126
- };
127
- if (settleMs > 0) await sleep(settleMs);
128
- }
129
-
130
- const error = new Error("Recruit candidate card is not inside the visible viewport before click");
131
- error.card_viewport_attempts = attempts;
132
- error.card_node_id = cardNodeId;
133
- throw error;
134
- }
135
-
136
- export function matchesRecruitDetailNetwork(url) {
137
- return RECRUIT_DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
138
- }
139
-
140
- export function createRecruitDetailNetworkRecorder(client) {
141
- const events = [];
142
- client.Network.responseReceived((event) => {
143
- const url = event?.response?.url || "";
144
- if (!matchesRecruitDetailNetwork(url)) return;
145
- events.push({
146
- requestId: event.requestId,
147
- url,
148
- status: event.response?.status,
149
- mimeType: event.response?.mimeType,
150
- type: event.type
151
- });
152
- });
153
- if (typeof client.Network.loadingFinished === "function") {
154
- client.Network.loadingFinished((event) => {
155
- const found = events.find((item) => item.requestId === event.requestId);
156
- if (!found) return;
157
- found.loading_finished = true;
158
- found.encodedDataLength = event.encodedDataLength;
159
- });
160
- }
161
- if (typeof client.Network.loadingFailed === "function") {
162
- client.Network.loadingFailed((event) => {
163
- const found = events.find((item) => item.requestId === event.requestId);
164
- if (!found) return;
165
- found.loading_failed = true;
166
- found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
167
- });
168
- }
169
- return {
170
- events,
171
- clear() {
172
- events.length = 0;
173
- }
174
- };
175
- }
176
-
177
- export async function findRecruitBlockingPanel(client, options = {}) {
178
- return findBossAccountRightsBlockingPanel(client, options);
179
- }
180
-
181
- export async function closeRecruitBlockingPanels(client, options = {}) {
182
- return closeBossAccountRightsBlockingPanel(client, {
183
- resolveRoots: getRecruitRoots,
184
- ...options
185
- });
186
- }
187
-
188
- export async function waitForRecruitDetailNetworkEvents(recorder, {
189
- minCount = 1,
190
- requireLoaded = true,
191
- timeoutMs = 3500,
192
- intervalMs = 100
193
- } = {}) {
194
- const started = Date.now();
195
- const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
196
- let matching = [];
197
- while (Date.now() - started <= timeoutMs) {
198
- matching = events.filter((event) => (
199
- !requireLoaded
200
- || event.loading_finished === true
201
- || event.loading_failed === true
202
- ));
203
- if (matching.length >= minCount) {
204
- return {
205
- ok: true,
206
- elapsed_ms: Date.now() - started,
207
- count: matching.length,
208
- events: matching
209
- };
210
- }
211
- await sleep(intervalMs);
212
- }
213
- return {
214
- ok: false,
215
- elapsed_ms: Date.now() - started,
216
- count: matching.length,
217
- events: matching,
218
- total_event_count: events.length
219
- };
220
- }
221
-
222
- export async function readRecruitDetailNetworkBodies(client, events = [], {
223
- limit = 10
224
- } = {}) {
225
- const bodies = [];
226
- for (const event of events.slice(0, limit)) {
227
- try {
228
- const body = await client.Network.getResponseBody({ requestId: event.requestId });
229
- bodies.push({
230
- ...event,
231
- body,
232
- body_length: String(body?.body || "").length
233
- });
234
- } catch (error) {
235
- bodies.push({
236
- ...event,
237
- body_error: error?.message || String(error)
238
- });
239
- }
240
- }
241
- return bodies;
242
- }
243
-
244
- export async function waitForRecruitDetail(client, {
245
- timeoutMs = 12000,
246
- intervalMs = 250
247
- } = {}) {
248
- const started = Date.now();
249
- let lastState = null;
250
- while (Date.now() - started <= timeoutMs) {
251
- const rootState = await getRecruitRoots(client);
252
- const popup = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
253
- const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
254
- lastState = {
255
- iframe: rootState.iframe,
256
- roots: rootState.roots,
257
- popup,
258
- resumeIframe
259
- };
260
- if (popup || resumeIframe) return lastState;
261
- await sleep(intervalMs);
262
- }
263
- return lastState;
264
- }
265
-
266
- async function findVisibleDetailTarget(client, roots, selectors) {
267
- for (const root of roots) {
268
- if (!root?.nodeId) continue;
269
- for (const selector of selectors) {
270
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
271
- for (const nodeId of nodeIds) {
272
- try {
273
- const box = await getNodeBox(client, nodeId);
274
- if (box.rect.width > 2 && box.rect.height > 2) {
275
- return {
276
- root: root.name,
277
- root_node_id: root.nodeId,
278
- selector,
279
- node_id: nodeId,
280
- center: box.center,
281
- rect: box.rect
282
- };
283
- }
284
- } catch {}
285
- }
286
- }
287
- }
288
- return null;
289
- }
290
-
291
- export async function readRecruitDetailHtml(client, detailState) {
292
- let popupHTML = "";
293
- let resumeHTML = "";
294
- let resumeIframeDocumentNodeId = null;
295
-
296
- if (detailState?.popup?.node_id) {
297
- popupHTML = await getOuterHTML(client, detailState.popup.node_id);
298
- }
299
-
300
- if (detailState?.resumeIframe?.node_id) {
301
- resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
302
- resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
303
- }
304
-
305
- return {
306
- popupHTML,
307
- resumeHTML,
308
- resumeIframeDocumentNodeId,
309
- popupText: htmlToText(popupHTML),
310
- resumeText: htmlToText(resumeHTML)
311
- };
312
- }
313
-
314
- export async function waitForRecruitDetailContent(client, {
315
- minTextLength = 200,
316
- timeoutMs = 6000,
317
- intervalMs = 200
318
- } = {}) {
319
- const started = Date.now();
320
- let lastState = null;
321
- let lastHtml = null;
322
- let lastError = null;
323
- while (Date.now() - started <= timeoutMs) {
324
- try {
325
- lastState = await waitForRecruitDetail(client, {
326
- timeoutMs: 500,
327
- intervalMs: 100
328
- });
329
- if (lastState?.popup || lastState?.resumeIframe) {
330
- lastHtml = await readRecruitDetailHtml(client, lastState);
331
- const textLength = (lastHtml.popupText || "").length + (lastHtml.resumeText || "").length;
332
- if (textLength >= minTextLength) {
333
- return {
334
- ok: true,
335
- elapsed_ms: Date.now() - started,
336
- text_length: textLength,
337
- detail_state: lastState,
338
- detail_html: lastHtml
339
- };
340
- }
341
- }
342
- } catch (error) {
343
- lastError = error;
344
- }
345
- await sleep(intervalMs);
346
- }
347
-
348
- const textLength = (lastHtml?.popupText || "").length + (lastHtml?.resumeText || "").length;
349
- return {
350
- ok: false,
351
- elapsed_ms: Date.now() - started,
352
- text_length: textLength,
353
- detail_state: lastState,
354
- detail_html: lastHtml,
355
- error: lastError?.message || null
356
- };
357
- }
358
-
359
- export async function openRecruitCardDetail(client, cardNodeId, {
360
- timeoutMs = 12000
361
- } = {}) {
362
- const openedStarted = Date.now();
363
- const attempts = [];
364
- const clickStarted = Date.now();
365
- const viewportGuard = await ensureRecruitCardInViewport(client, cardNodeId);
366
- const cardBox = await clickNodeCenter(client, cardNodeId, {
367
- scrollIntoView: false
368
- });
369
- let candidateClickMs = Date.now() - clickStarted;
370
- attempts.push({
371
- mode: "card-center",
372
- center: cardBox.center,
373
- viewport_guard: {
374
- ok: viewportGuard.ok,
375
- verified: viewportGuard.verified,
376
- attempts: viewportGuard.attempts
377
- }
378
- });
379
- const detailStarted = Date.now();
380
- let detailState = await waitForRecruitDetail(client, { timeoutMs });
381
-
382
- if (!detailState?.popup && !detailState?.resumeIframe) {
383
- const fallbackClickStarted = Date.now();
384
- const leftTitlePoint = {
385
- x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
386
- y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
387
- };
388
- await clickPoint(client, leftTitlePoint.x, leftTitlePoint.y, {
389
- clickCount: 2,
390
- delayMs: 120
391
- });
392
- candidateClickMs += Date.now() - fallbackClickStarted;
393
- attempts.push({
394
- mode: "card-left-title-double-click",
395
- center: leftTitlePoint
396
- });
397
- detailState = await waitForRecruitDetail(client, {
398
- timeoutMs: Math.max(3000, Math.floor(timeoutMs / 2))
399
- });
400
- }
401
-
402
- if (!detailState?.popup && !detailState?.resumeIframe) {
403
- throw new Error("Recruit candidate detail did not open or no known detail selectors mounted");
404
- }
405
-
406
- return {
407
- card_box: cardBox,
408
- open_attempts: attempts,
409
- detail_state: detailState,
410
- timings: {
411
- candidate_click_ms: candidateClickMs,
412
- detail_open_ms: Date.now() - detailStarted,
413
- open_total_ms: Date.now() - openedStarted
414
- }
415
- };
416
- }
417
-
418
- export async function closeRecruitDetail(client, {
419
- attemptsLimit = 3
420
- } = {}) {
421
- const attempts = [];
422
- for (let index = 0; index < attemptsLimit; index += 1) {
423
- const existingState = await waitForRecruitDetail(client, { timeoutMs: 500 });
424
- if (!existingState?.popup && !existingState?.resumeIframe) {
425
- return {
426
- closed: true,
427
- attempts
428
- };
429
- }
430
-
431
- const rootState = await getRecruitRoots(client);
432
- const closeTarget = await findVisibleCloseTarget(client, rootState.roots, RECRUIT_DETAIL_CLOSE_SELECTORS);
433
- if (closeTarget) {
434
- try {
435
- if (closeTarget.center) {
436
- await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
437
- } else {
438
- await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
439
- }
440
- attempts.push({
441
- mode: "close-selector",
442
- selector: closeTarget.selector,
443
- root: closeTarget.root
444
- });
445
- } catch (error) {
446
- attempts.push({
447
- mode: "close-selector-error",
448
- selector: closeTarget.selector,
449
- root: closeTarget.root,
450
- error: error?.message || String(error)
451
- });
452
- await pressEscape(client);
453
- attempts.push({ mode: "Escape-after-close-selector-error" });
454
- }
455
- await sleep(700);
456
- } else {
457
- await pressEscape(client);
458
- attempts.push({ mode: "Escape" });
459
- await sleep(700);
460
- }
461
-
462
- let state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
463
- if (!state?.popup && !state?.resumeIframe) {
464
- return {
465
- closed: true,
466
- attempts
467
- };
468
- }
469
-
470
- await pressEscape(client);
471
- attempts.push({ mode: "Escape-fallback" });
472
- await sleep(700);
473
-
474
- state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
475
- if (!state?.popup && !state?.resumeIframe) {
476
- return {
477
- closed: true,
478
- attempts
479
- };
480
- }
481
- }
482
-
483
- return {
484
- closed: false,
485
- attempts
486
- };
487
- }
488
-
489
- async function findVisibleCloseTarget(client, roots, selectors) {
490
- let fallback = null;
491
- for (const root of roots) {
492
- if (!root?.nodeId) continue;
493
- for (const selector of selectors) {
494
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
495
- for (const nodeId of nodeIds) {
496
- const target = {
497
- root: root.name,
498
- root_node_id: root.nodeId,
499
- selector,
500
- node_id: nodeId
501
- };
502
- if (!fallback) fallback = target;
503
- try {
504
- const box = await getNodeBox(client, nodeId);
505
- if (box.rect.width > 2 && box.rect.height > 2) {
506
- return {
507
- ...target,
508
- center: box.center,
509
- rect: box.rect
510
- };
511
- }
512
- } catch {}
513
- }
514
- }
515
- }
516
- return fallback;
517
- }
518
-
519
- async function pressEscape(client) {
520
- await pressKey(client, "Escape", {
521
- code: "Escape",
522
- windowsVirtualKeyCode: 27,
523
- nativeVirtualKeyCode: 27
524
- });
525
- }
526
-
527
- export async function extractRecruitDetailCandidate(client, {
528
- cardCandidate,
529
- cardNodeId,
530
- detailState,
531
- detailHtml: providedDetailHtml = null,
532
- networkEvents = [],
533
- targetUrl = "",
534
- closeDetail = true,
535
- networkParseRetryMs = 1800,
536
- networkParseIntervalMs = 250
537
- } = {}) {
538
- const detailHtml = providedDetailHtml || await readRecruitDetailHtml(client, detailState);
539
- const detailText = [
540
- detailHtml.popupText,
541
- detailHtml.resumeText
542
- ].filter(Boolean).join("\n\n");
543
-
544
- const parseStarted = Date.now();
545
- let networkBodies = [];
546
- let detailCandidateResult = null;
547
- do {
548
- networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
549
- detailCandidateResult = buildScreeningCandidateFromDetail({
550
- domain: "recruit",
551
- cardCandidate,
552
- detailText,
553
- networkBodies,
554
- metadata: {
555
- target_url: targetUrl,
556
- card_node_id: cardNodeId,
557
- detail_popup_selector: detailState?.popup?.selector || null,
558
- detail_popup_root: detailState?.popup?.root || null,
559
- resume_iframe_selector: detailState?.resumeIframe?.selector || null,
560
- resume_iframe_root: detailState?.resumeIframe?.root || null,
561
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
562
- }
563
- });
564
- if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
565
- if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
566
- await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
567
- } while (true);
568
-
569
- let closeResult = null;
570
- if (closeDetail) {
571
- closeResult = await closeRecruitDetail(client);
572
- }
573
-
574
- return {
575
- candidate: detailCandidateResult.candidate,
576
- parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
577
- network_bodies: networkBodies,
578
- network_parse_retry_elapsed_ms: Date.now() - parseStarted,
579
- network_event_count: networkEvents.length,
580
- detail: {
581
- popup_text: detailHtml.popupText,
582
- resume_text: detailHtml.resumeText,
583
- popup_html_length: detailHtml.popupHTML.length,
584
- resume_html_length: detailHtml.resumeHTML.length
585
- },
586
- close_result: closeResult
587
- };
588
- }
1
+ import {
2
+ clickNodeCenter,
3
+ clickPoint,
4
+ DETERMINISTIC_CLICK_OPTIONS,
5
+ getFrameDocumentNodeId,
6
+ getNodeBox,
7
+ getOuterHTML,
8
+ pressKey,
9
+ querySelectorAll,
10
+ scrollNodeIntoView,
11
+ sleep
12
+ } from "../../core/browser/index.js";
13
+ import {
14
+ buildScreeningCandidateFromDetail,
15
+ htmlToText
16
+ } from "../../core/screening/index.js";
17
+ import {
18
+ closeBossAccountRightsBlockingPanel,
19
+ findBossAccountRightsBlockingPanel
20
+ } from "../common/account-rights-panel.js";
21
+ import {
22
+ RECRUIT_DETAIL_CLOSE_SELECTORS,
23
+ RECRUIT_DETAIL_NETWORK_PATTERNS,
24
+ RECRUIT_DETAIL_POPUP_SELECTORS,
25
+ RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS
26
+ } from "./constants.js";
27
+ import {
28
+ getRecruitRoots
29
+ } from "./roots.js";
30
+
31
+ function compactBox(box = null) {
32
+ if (!box) return null;
33
+ return {
34
+ center: box.center || null,
35
+ rect: box.rect || null
36
+ };
37
+ }
38
+
39
+ async function getViewportRect(client) {
40
+ if (typeof client?.Page?.getLayoutMetrics !== "function") return null;
41
+ try {
42
+ const metrics = await client.Page.getLayoutMetrics();
43
+ const viewport = metrics?.cssVisualViewport || metrics?.visualViewport || metrics?.layoutViewport || {};
44
+ const width = Number(viewport.clientWidth || viewport.width || metrics?.layoutViewport?.clientWidth || 0);
45
+ const height = Number(viewport.clientHeight || viewport.height || metrics?.layoutViewport?.clientHeight || 0);
46
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
47
+ return {
48
+ x: 0,
49
+ y: 0,
50
+ width,
51
+ height
52
+ };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function boxCenterIsInViewport(box, viewport, { marginPx = 16 } = {}) {
59
+ if (!box?.center || !viewport) return null;
60
+ const margin = Math.max(0, Number(marginPx) || 0);
61
+ return (
62
+ box.center.x >= viewport.x + margin
63
+ && box.center.x <= viewport.x + viewport.width - margin
64
+ && box.center.y >= viewport.y + margin
65
+ && box.center.y <= viewport.y + viewport.height - margin
66
+ );
67
+ }
68
+
69
+ function scrollDeltaForBox(box, viewport) {
70
+ if (!box?.center || !viewport) return 0;
71
+ const targetY = viewport.y + viewport.height * 0.48;
72
+ const delta = box.center.y - targetY;
73
+ if (Math.abs(delta) < 80) return 0;
74
+ return Math.max(-900, Math.min(900, delta));
75
+ }
76
+
77
+ export async function ensureRecruitCardInViewport(client, cardNodeId, {
78
+ maxScrollAttempts = 4,
79
+ marginPx = 16,
80
+ settleMs = 220
81
+ } = {}) {
82
+ const attempts = [];
83
+ await scrollNodeIntoView(client, cardNodeId);
84
+ if (settleMs > 0) await sleep(settleMs);
85
+
86
+ let finalBox = null;
87
+ for (let attempt = 0; attempt <= maxScrollAttempts; attempt += 1) {
88
+ const box = await getNodeBox(client, cardNodeId);
89
+ finalBox = box;
90
+ const viewport = await getViewportRect(client);
91
+ const inViewport = boxCenterIsInViewport(box, viewport, { marginPx });
92
+ const entry = {
93
+ attempt,
94
+ in_viewport: inViewport,
95
+ viewport,
96
+ box: compactBox(box)
97
+ };
98
+ attempts.push(entry);
99
+ if (inViewport === true || inViewport === null) {
100
+ return {
101
+ ok: inViewport !== false,
102
+ verified: inViewport === true,
103
+ box,
104
+ attempts
105
+ };
106
+ }
107
+ if (attempt >= maxScrollAttempts) break;
108
+
109
+ const deltaY = scrollDeltaForBox(box, viewport);
110
+ if (!deltaY) break;
111
+ const wheelPoint = {
112
+ x: viewport.x + viewport.width * 0.5,
113
+ y: viewport.y + viewport.height * 0.5
114
+ };
115
+ await client.Input.dispatchMouseEvent({
116
+ type: "mouseWheel",
117
+ x: wheelPoint.x,
118
+ y: wheelPoint.y,
119
+ deltaX: 0,
120
+ deltaY
121
+ });
122
+ entry.scroll = {
123
+ method: "mouseWheel",
124
+ delta_y: deltaY,
125
+ point: wheelPoint
126
+ };
127
+ if (settleMs > 0) await sleep(settleMs);
128
+ }
129
+
130
+ const error = new Error("Recruit candidate card is not inside the visible viewport before click");
131
+ error.card_viewport_attempts = attempts;
132
+ error.card_node_id = cardNodeId;
133
+ throw error;
134
+ }
135
+
136
+ export function matchesRecruitDetailNetwork(url) {
137
+ return RECRUIT_DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
138
+ }
139
+
140
+ export function createRecruitDetailNetworkRecorder(client) {
141
+ const events = [];
142
+ client.Network.responseReceived((event) => {
143
+ const url = event?.response?.url || "";
144
+ if (!matchesRecruitDetailNetwork(url)) return;
145
+ events.push({
146
+ requestId: event.requestId,
147
+ url,
148
+ status: event.response?.status,
149
+ mimeType: event.response?.mimeType,
150
+ type: event.type
151
+ });
152
+ });
153
+ if (typeof client.Network.loadingFinished === "function") {
154
+ client.Network.loadingFinished((event) => {
155
+ const found = events.find((item) => item.requestId === event.requestId);
156
+ if (!found) return;
157
+ found.loading_finished = true;
158
+ found.encodedDataLength = event.encodedDataLength;
159
+ });
160
+ }
161
+ if (typeof client.Network.loadingFailed === "function") {
162
+ client.Network.loadingFailed((event) => {
163
+ const found = events.find((item) => item.requestId === event.requestId);
164
+ if (!found) return;
165
+ found.loading_failed = true;
166
+ found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
167
+ });
168
+ }
169
+ return {
170
+ events,
171
+ clear() {
172
+ events.length = 0;
173
+ }
174
+ };
175
+ }
176
+
177
+ export async function findRecruitBlockingPanel(client, options = {}) {
178
+ return findBossAccountRightsBlockingPanel(client, options);
179
+ }
180
+
181
+ export async function closeRecruitBlockingPanels(client, options = {}) {
182
+ return closeBossAccountRightsBlockingPanel(client, {
183
+ resolveRoots: getRecruitRoots,
184
+ ...options
185
+ });
186
+ }
187
+
188
+ export async function waitForRecruitDetailNetworkEvents(recorder, {
189
+ minCount = 1,
190
+ requireLoaded = true,
191
+ timeoutMs = 3500,
192
+ intervalMs = 100
193
+ } = {}) {
194
+ const started = Date.now();
195
+ const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
196
+ let matching = [];
197
+ while (Date.now() - started <= timeoutMs) {
198
+ matching = events.filter((event) => (
199
+ !requireLoaded
200
+ || event.loading_finished === true
201
+ || event.loading_failed === true
202
+ ));
203
+ if (matching.length >= minCount) {
204
+ return {
205
+ ok: true,
206
+ elapsed_ms: Date.now() - started,
207
+ count: matching.length,
208
+ events: matching
209
+ };
210
+ }
211
+ await sleep(intervalMs);
212
+ }
213
+ return {
214
+ ok: false,
215
+ elapsed_ms: Date.now() - started,
216
+ count: matching.length,
217
+ events: matching,
218
+ total_event_count: events.length
219
+ };
220
+ }
221
+
222
+ export async function readRecruitDetailNetworkBodies(client, events = [], {
223
+ limit = 10
224
+ } = {}) {
225
+ const bodies = [];
226
+ for (const event of events.slice(0, limit)) {
227
+ try {
228
+ const body = await client.Network.getResponseBody({ requestId: event.requestId });
229
+ bodies.push({
230
+ ...event,
231
+ body,
232
+ body_length: String(body?.body || "").length
233
+ });
234
+ } catch (error) {
235
+ bodies.push({
236
+ ...event,
237
+ body_error: error?.message || String(error)
238
+ });
239
+ }
240
+ }
241
+ return bodies;
242
+ }
243
+
244
+ export async function waitForRecruitDetail(client, {
245
+ timeoutMs = 12000,
246
+ intervalMs = 250
247
+ } = {}) {
248
+ const started = Date.now();
249
+ let lastState = null;
250
+ while (Date.now() - started <= timeoutMs) {
251
+ const rootState = await getRecruitRoots(client);
252
+ const popup = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
253
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
254
+ lastState = {
255
+ iframe: rootState.iframe,
256
+ roots: rootState.roots,
257
+ popup,
258
+ resumeIframe
259
+ };
260
+ if (popup || resumeIframe) return lastState;
261
+ await sleep(intervalMs);
262
+ }
263
+ return lastState;
264
+ }
265
+
266
+ async function findVisibleDetailTarget(client, roots, selectors) {
267
+ for (const root of roots) {
268
+ if (!root?.nodeId) continue;
269
+ for (const selector of selectors) {
270
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
271
+ for (const nodeId of nodeIds) {
272
+ try {
273
+ const box = await getNodeBox(client, nodeId);
274
+ if (box.rect.width > 2 && box.rect.height > 2) {
275
+ return {
276
+ root: root.name,
277
+ root_node_id: root.nodeId,
278
+ selector,
279
+ node_id: nodeId,
280
+ center: box.center,
281
+ rect: box.rect
282
+ };
283
+ }
284
+ } catch {}
285
+ }
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+
291
+ export async function readRecruitDetailHtml(client, detailState) {
292
+ let popupHTML = "";
293
+ let resumeHTML = "";
294
+ let resumeIframeDocumentNodeId = null;
295
+
296
+ if (detailState?.popup?.node_id) {
297
+ popupHTML = await getOuterHTML(client, detailState.popup.node_id);
298
+ }
299
+
300
+ if (detailState?.resumeIframe?.node_id) {
301
+ resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
302
+ resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
303
+ }
304
+
305
+ return {
306
+ popupHTML,
307
+ resumeHTML,
308
+ resumeIframeDocumentNodeId,
309
+ popupText: htmlToText(popupHTML),
310
+ resumeText: htmlToText(resumeHTML)
311
+ };
312
+ }
313
+
314
+ export async function waitForRecruitDetailContent(client, {
315
+ minTextLength = 200,
316
+ timeoutMs = 6000,
317
+ intervalMs = 200
318
+ } = {}) {
319
+ const started = Date.now();
320
+ let lastState = null;
321
+ let lastHtml = null;
322
+ let lastError = null;
323
+ while (Date.now() - started <= timeoutMs) {
324
+ try {
325
+ lastState = await waitForRecruitDetail(client, {
326
+ timeoutMs: 500,
327
+ intervalMs: 100
328
+ });
329
+ if (lastState?.popup || lastState?.resumeIframe) {
330
+ lastHtml = await readRecruitDetailHtml(client, lastState);
331
+ const textLength = (lastHtml.popupText || "").length + (lastHtml.resumeText || "").length;
332
+ if (textLength >= minTextLength) {
333
+ return {
334
+ ok: true,
335
+ elapsed_ms: Date.now() - started,
336
+ text_length: textLength,
337
+ detail_state: lastState,
338
+ detail_html: lastHtml
339
+ };
340
+ }
341
+ }
342
+ } catch (error) {
343
+ lastError = error;
344
+ }
345
+ await sleep(intervalMs);
346
+ }
347
+
348
+ const textLength = (lastHtml?.popupText || "").length + (lastHtml?.resumeText || "").length;
349
+ return {
350
+ ok: false,
351
+ elapsed_ms: Date.now() - started,
352
+ text_length: textLength,
353
+ detail_state: lastState,
354
+ detail_html: lastHtml,
355
+ error: lastError?.message || null
356
+ };
357
+ }
358
+
359
+ export async function openRecruitCardDetail(client, cardNodeId, {
360
+ timeoutMs = 12000
361
+ } = {}) {
362
+ const openedStarted = Date.now();
363
+ const attempts = [];
364
+ const clickStarted = Date.now();
365
+ const viewportGuard = await ensureRecruitCardInViewport(client, cardNodeId);
366
+ const cardBox = await clickNodeCenter(client, cardNodeId, {
367
+ scrollIntoView: false
368
+ });
369
+ let candidateClickMs = Date.now() - clickStarted;
370
+ attempts.push({
371
+ mode: "card-center",
372
+ center: cardBox.center,
373
+ viewport_guard: {
374
+ ok: viewportGuard.ok,
375
+ verified: viewportGuard.verified,
376
+ attempts: viewportGuard.attempts
377
+ }
378
+ });
379
+ const detailStarted = Date.now();
380
+ let detailState = await waitForRecruitDetail(client, { timeoutMs });
381
+
382
+ if (!detailState?.popup && !detailState?.resumeIframe) {
383
+ const fallbackClickStarted = Date.now();
384
+ const leftTitlePoint = {
385
+ x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
386
+ y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
387
+ };
388
+ await clickPoint(client, leftTitlePoint.x, leftTitlePoint.y, {
389
+ clickCount: 2,
390
+ delayMs: 120
391
+ });
392
+ candidateClickMs += Date.now() - fallbackClickStarted;
393
+ attempts.push({
394
+ mode: "card-left-title-double-click",
395
+ center: leftTitlePoint
396
+ });
397
+ detailState = await waitForRecruitDetail(client, {
398
+ timeoutMs: Math.max(3000, Math.floor(timeoutMs / 2))
399
+ });
400
+ }
401
+
402
+ if (!detailState?.popup && !detailState?.resumeIframe) {
403
+ throw new Error("Recruit candidate detail did not open or no known detail selectors mounted");
404
+ }
405
+
406
+ return {
407
+ card_box: cardBox,
408
+ open_attempts: attempts,
409
+ detail_state: detailState,
410
+ timings: {
411
+ candidate_click_ms: candidateClickMs,
412
+ detail_open_ms: Date.now() - detailStarted,
413
+ open_total_ms: Date.now() - openedStarted
414
+ }
415
+ };
416
+ }
417
+
418
+ export async function closeRecruitDetail(client, {
419
+ attemptsLimit = 3
420
+ } = {}) {
421
+ const attempts = [];
422
+ for (let index = 0; index < attemptsLimit; index += 1) {
423
+ const existingState = await waitForRecruitDetail(client, { timeoutMs: 500 });
424
+ if (!existingState?.popup && !existingState?.resumeIframe) {
425
+ return {
426
+ closed: true,
427
+ attempts
428
+ };
429
+ }
430
+
431
+ const rootState = await getRecruitRoots(client);
432
+ const closeTarget = await findVisibleCloseTarget(client, rootState.roots, RECRUIT_DETAIL_CLOSE_SELECTORS);
433
+ if (closeTarget) {
434
+ try {
435
+ if (closeTarget.center) {
436
+ await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
437
+ } else {
438
+ await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
439
+ }
440
+ attempts.push({
441
+ mode: "close-selector",
442
+ selector: closeTarget.selector,
443
+ root: closeTarget.root
444
+ });
445
+ } catch (error) {
446
+ attempts.push({
447
+ mode: "close-selector-error",
448
+ selector: closeTarget.selector,
449
+ root: closeTarget.root,
450
+ error: error?.message || String(error)
451
+ });
452
+ await pressEscape(client);
453
+ attempts.push({ mode: "Escape-after-close-selector-error" });
454
+ }
455
+ await sleep(700);
456
+ } else {
457
+ await pressEscape(client);
458
+ attempts.push({ mode: "Escape" });
459
+ await sleep(700);
460
+ }
461
+
462
+ let state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
463
+ if (!state?.popup && !state?.resumeIframe) {
464
+ return {
465
+ closed: true,
466
+ attempts
467
+ };
468
+ }
469
+
470
+ await pressEscape(client);
471
+ attempts.push({ mode: "Escape-fallback" });
472
+ await sleep(700);
473
+
474
+ state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
475
+ if (!state?.popup && !state?.resumeIframe) {
476
+ return {
477
+ closed: true,
478
+ attempts
479
+ };
480
+ }
481
+ }
482
+
483
+ return {
484
+ closed: false,
485
+ attempts
486
+ };
487
+ }
488
+
489
+ async function findVisibleCloseTarget(client, roots, selectors) {
490
+ let fallback = null;
491
+ for (const root of roots) {
492
+ if (!root?.nodeId) continue;
493
+ for (const selector of selectors) {
494
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
495
+ for (const nodeId of nodeIds) {
496
+ const target = {
497
+ root: root.name,
498
+ root_node_id: root.nodeId,
499
+ selector,
500
+ node_id: nodeId
501
+ };
502
+ if (!fallback) fallback = target;
503
+ try {
504
+ const box = await getNodeBox(client, nodeId);
505
+ if (box.rect.width > 2 && box.rect.height > 2) {
506
+ return {
507
+ ...target,
508
+ center: box.center,
509
+ rect: box.rect
510
+ };
511
+ }
512
+ } catch {}
513
+ }
514
+ }
515
+ }
516
+ return fallback;
517
+ }
518
+
519
+ async function pressEscape(client) {
520
+ await pressKey(client, "Escape", {
521
+ code: "Escape",
522
+ windowsVirtualKeyCode: 27,
523
+ nativeVirtualKeyCode: 27
524
+ });
525
+ }
526
+
527
+ export async function extractRecruitDetailCandidate(client, {
528
+ cardCandidate,
529
+ cardNodeId,
530
+ detailState,
531
+ detailHtml: providedDetailHtml = null,
532
+ networkEvents = [],
533
+ targetUrl = "",
534
+ closeDetail = true,
535
+ networkParseRetryMs = 1800,
536
+ networkParseIntervalMs = 250
537
+ } = {}) {
538
+ const detailHtml = providedDetailHtml || await readRecruitDetailHtml(client, detailState);
539
+ const detailText = [
540
+ detailHtml.popupText,
541
+ detailHtml.resumeText
542
+ ].filter(Boolean).join("\n\n");
543
+
544
+ const parseStarted = Date.now();
545
+ let networkBodies = [];
546
+ let detailCandidateResult = null;
547
+ do {
548
+ networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
549
+ detailCandidateResult = buildScreeningCandidateFromDetail({
550
+ domain: "recruit",
551
+ cardCandidate,
552
+ detailText,
553
+ networkBodies,
554
+ metadata: {
555
+ target_url: targetUrl,
556
+ card_node_id: cardNodeId,
557
+ detail_popup_selector: detailState?.popup?.selector || null,
558
+ detail_popup_root: detailState?.popup?.root || null,
559
+ resume_iframe_selector: detailState?.resumeIframe?.selector || null,
560
+ resume_iframe_root: detailState?.resumeIframe?.root || null,
561
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
562
+ }
563
+ });
564
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
565
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
566
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
567
+ } while (true);
568
+
569
+ let closeResult = null;
570
+ if (closeDetail) {
571
+ closeResult = await closeRecruitDetail(client);
572
+ }
573
+
574
+ return {
575
+ candidate: detailCandidateResult.candidate,
576
+ parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
577
+ network_bodies: networkBodies,
578
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
579
+ network_event_count: networkEvents.length,
580
+ detail: {
581
+ popup_text: detailHtml.popupText,
582
+ resume_text: detailHtml.resumeText,
583
+ popup_html_length: detailHtml.popupHTML.length,
584
+ resume_html_length: detailHtml.resumeHTML.length
585
+ },
586
+ close_result: closeResult
587
+ };
588
+ }