@reconcrap/boss-recommend-mcp 2.0.45 → 2.0.47

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 (56) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1453 -1446
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/screening/index.js +50 -3
  24. package/src/core/self-heal/index.js +973 -973
  25. package/src/core/self-heal/viewport.js +564 -564
  26. package/src/domains/chat/cards.js +137 -137
  27. package/src/domains/chat/constants.js +221 -221
  28. package/src/domains/chat/detail.js +1668 -1661
  29. package/src/domains/chat/index.js +7 -7
  30. package/src/domains/chat/jobs.js +592 -588
  31. package/src/domains/chat/page-guard.js +98 -98
  32. package/src/domains/chat/roots.js +56 -56
  33. package/src/domains/chat/run-service.js +1977 -1955
  34. package/src/domains/recommend/actions.js +457 -457
  35. package/src/domains/recommend/cards.js +243 -243
  36. package/src/domains/recommend/constants.js +165 -165
  37. package/src/domains/recommend/detail.js +36 -28
  38. package/src/domains/recommend/filters.js +610 -581
  39. package/src/domains/recommend/index.js +10 -10
  40. package/src/domains/recommend/jobs.js +316 -263
  41. package/src/domains/recommend/refresh.js +472 -472
  42. package/src/domains/recommend/roots.js +80 -80
  43. package/src/domains/recommend/run-service.js +75 -35
  44. package/src/domains/recommend/scopes.js +246 -245
  45. package/src/domains/recruit/actions.js +277 -277
  46. package/src/domains/recruit/cards.js +74 -74
  47. package/src/domains/recruit/constants.js +167 -167
  48. package/src/domains/recruit/detail.js +461 -460
  49. package/src/domains/recruit/index.js +9 -9
  50. package/src/domains/recruit/instruction-parser.js +451 -451
  51. package/src/domains/recruit/refresh.js +44 -44
  52. package/src/domains/recruit/roots.js +68 -68
  53. package/src/domains/recruit/run-service.js +1207 -1161
  54. package/src/domains/recruit/search.js +1202 -1149
  55. package/src/recommend-mcp.js +22 -22
  56. package/src/recruit-mcp.js +1338 -1338
@@ -1,460 +1,461 @@
1
- import {
2
- clickNodeCenter,
3
- clickPoint,
4
- getFrameDocumentNodeId,
5
- getNodeBox,
6
- getOuterHTML,
7
- pressKey,
8
- querySelectorAll,
9
- sleep
10
- } from "../../core/browser/index.js";
11
- import {
12
- buildScreeningCandidateFromDetail,
13
- htmlToText
14
- } from "../../core/screening/index.js";
15
- import {
16
- RECRUIT_DETAIL_CLOSE_SELECTORS,
17
- RECRUIT_DETAIL_NETWORK_PATTERNS,
18
- RECRUIT_DETAIL_POPUP_SELECTORS,
19
- RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS
20
- } from "./constants.js";
21
- import {
22
- getRecruitRoots
23
- } from "./roots.js";
24
-
25
- export function matchesRecruitDetailNetwork(url) {
26
- return RECRUIT_DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
27
- }
28
-
29
- export function createRecruitDetailNetworkRecorder(client) {
30
- const events = [];
31
- client.Network.responseReceived((event) => {
32
- const url = event?.response?.url || "";
33
- if (!matchesRecruitDetailNetwork(url)) return;
34
- events.push({
35
- requestId: event.requestId,
36
- url,
37
- status: event.response?.status,
38
- mimeType: event.response?.mimeType,
39
- type: event.type
40
- });
41
- });
42
- if (typeof client.Network.loadingFinished === "function") {
43
- client.Network.loadingFinished((event) => {
44
- const found = events.find((item) => item.requestId === event.requestId);
45
- if (!found) return;
46
- found.loading_finished = true;
47
- found.encodedDataLength = event.encodedDataLength;
48
- });
49
- }
50
- if (typeof client.Network.loadingFailed === "function") {
51
- client.Network.loadingFailed((event) => {
52
- const found = events.find((item) => item.requestId === event.requestId);
53
- if (!found) return;
54
- found.loading_failed = true;
55
- found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
56
- });
57
- }
58
- return {
59
- events,
60
- clear() {
61
- events.length = 0;
62
- }
63
- };
64
- }
65
-
66
- export async function waitForRecruitDetailNetworkEvents(recorder, {
67
- minCount = 1,
68
- requireLoaded = true,
69
- timeoutMs = 3500,
70
- intervalMs = 100
71
- } = {}) {
72
- const started = Date.now();
73
- const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
74
- let matching = [];
75
- while (Date.now() - started <= timeoutMs) {
76
- matching = events.filter((event) => (
77
- !requireLoaded
78
- || event.loading_finished === true
79
- || event.loading_failed === true
80
- ));
81
- if (matching.length >= minCount) {
82
- return {
83
- ok: true,
84
- elapsed_ms: Date.now() - started,
85
- count: matching.length,
86
- events: matching
87
- };
88
- }
89
- await sleep(intervalMs);
90
- }
91
- return {
92
- ok: false,
93
- elapsed_ms: Date.now() - started,
94
- count: matching.length,
95
- events: matching,
96
- total_event_count: events.length
97
- };
98
- }
99
-
100
- export async function readRecruitDetailNetworkBodies(client, events = [], {
101
- limit = 10
102
- } = {}) {
103
- const bodies = [];
104
- for (const event of events.slice(0, limit)) {
105
- try {
106
- const body = await client.Network.getResponseBody({ requestId: event.requestId });
107
- bodies.push({
108
- ...event,
109
- body,
110
- body_length: String(body?.body || "").length
111
- });
112
- } catch (error) {
113
- bodies.push({
114
- ...event,
115
- body_error: error?.message || String(error)
116
- });
117
- }
118
- }
119
- return bodies;
120
- }
121
-
122
- export async function waitForRecruitDetail(client, {
123
- timeoutMs = 12000,
124
- intervalMs = 250
125
- } = {}) {
126
- const started = Date.now();
127
- let lastState = null;
128
- while (Date.now() - started <= timeoutMs) {
129
- const rootState = await getRecruitRoots(client);
130
- const popup = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
131
- const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
132
- lastState = {
133
- iframe: rootState.iframe,
134
- roots: rootState.roots,
135
- popup,
136
- resumeIframe
137
- };
138
- if (popup || resumeIframe) return lastState;
139
- await sleep(intervalMs);
140
- }
141
- return lastState;
142
- }
143
-
144
- async function findVisibleDetailTarget(client, roots, selectors) {
145
- for (const root of roots) {
146
- if (!root?.nodeId) continue;
147
- for (const selector of selectors) {
148
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
149
- for (const nodeId of nodeIds) {
150
- try {
151
- const box = await getNodeBox(client, nodeId);
152
- if (box.rect.width > 2 && box.rect.height > 2) {
153
- return {
154
- root: root.name,
155
- root_node_id: root.nodeId,
156
- selector,
157
- node_id: nodeId,
158
- center: box.center,
159
- rect: box.rect
160
- };
161
- }
162
- } catch {}
163
- }
164
- }
165
- }
166
- return null;
167
- }
168
-
169
- export async function readRecruitDetailHtml(client, detailState) {
170
- let popupHTML = "";
171
- let resumeHTML = "";
172
- let resumeIframeDocumentNodeId = null;
173
-
174
- if (detailState?.popup?.node_id) {
175
- popupHTML = await getOuterHTML(client, detailState.popup.node_id);
176
- }
177
-
178
- if (detailState?.resumeIframe?.node_id) {
179
- resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
180
- resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
181
- }
182
-
183
- return {
184
- popupHTML,
185
- resumeHTML,
186
- resumeIframeDocumentNodeId,
187
- popupText: htmlToText(popupHTML),
188
- resumeText: htmlToText(resumeHTML)
189
- };
190
- }
191
-
192
- export async function waitForRecruitDetailContent(client, {
193
- minTextLength = 200,
194
- timeoutMs = 6000,
195
- intervalMs = 200
196
- } = {}) {
197
- const started = Date.now();
198
- let lastState = null;
199
- let lastHtml = null;
200
- let lastError = null;
201
- while (Date.now() - started <= timeoutMs) {
202
- try {
203
- lastState = await waitForRecruitDetail(client, {
204
- timeoutMs: 500,
205
- intervalMs: 100
206
- });
207
- if (lastState?.popup || lastState?.resumeIframe) {
208
- lastHtml = await readRecruitDetailHtml(client, lastState);
209
- const textLength = (lastHtml.popupText || "").length + (lastHtml.resumeText || "").length;
210
- if (textLength >= minTextLength) {
211
- return {
212
- ok: true,
213
- elapsed_ms: Date.now() - started,
214
- text_length: textLength,
215
- detail_state: lastState,
216
- detail_html: lastHtml
217
- };
218
- }
219
- }
220
- } catch (error) {
221
- lastError = error;
222
- }
223
- await sleep(intervalMs);
224
- }
225
-
226
- const textLength = (lastHtml?.popupText || "").length + (lastHtml?.resumeText || "").length;
227
- return {
228
- ok: false,
229
- elapsed_ms: Date.now() - started,
230
- text_length: textLength,
231
- detail_state: lastState,
232
- detail_html: lastHtml,
233
- error: lastError?.message || null
234
- };
235
- }
236
-
237
- export async function openRecruitCardDetail(client, cardNodeId, {
238
- timeoutMs = 12000
239
- } = {}) {
240
- const openedStarted = Date.now();
241
- const attempts = [];
242
- const clickStarted = Date.now();
243
- const cardBox = await clickNodeCenter(client, cardNodeId, {
244
- scrollIntoView: true
245
- });
246
- let candidateClickMs = Date.now() - clickStarted;
247
- attempts.push({
248
- mode: "card-center",
249
- center: cardBox.center
250
- });
251
- const detailStarted = Date.now();
252
- let detailState = await waitForRecruitDetail(client, { timeoutMs });
253
-
254
- if (!detailState?.popup && !detailState?.resumeIframe) {
255
- const fallbackClickStarted = Date.now();
256
- const leftTitlePoint = {
257
- x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
258
- y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
259
- };
260
- await clickPoint(client, leftTitlePoint.x, leftTitlePoint.y, {
261
- clickCount: 2,
262
- delayMs: 120
263
- });
264
- candidateClickMs += Date.now() - fallbackClickStarted;
265
- attempts.push({
266
- mode: "card-left-title-double-click",
267
- center: leftTitlePoint
268
- });
269
- detailState = await waitForRecruitDetail(client, {
270
- timeoutMs: Math.max(3000, Math.floor(timeoutMs / 2))
271
- });
272
- }
273
-
274
- if (!detailState?.popup && !detailState?.resumeIframe) {
275
- throw new Error("Recruit candidate detail did not open or no known detail selectors mounted");
276
- }
277
-
278
- return {
279
- card_box: cardBox,
280
- open_attempts: attempts,
281
- detail_state: detailState,
282
- timings: {
283
- candidate_click_ms: candidateClickMs,
284
- detail_open_ms: Date.now() - detailStarted,
285
- open_total_ms: Date.now() - openedStarted
286
- }
287
- };
288
- }
289
-
290
- export async function closeRecruitDetail(client, {
291
- attemptsLimit = 3
292
- } = {}) {
293
- const attempts = [];
294
- for (let index = 0; index < attemptsLimit; index += 1) {
295
- const existingState = await waitForRecruitDetail(client, { timeoutMs: 500 });
296
- if (!existingState?.popup && !existingState?.resumeIframe) {
297
- return {
298
- closed: true,
299
- attempts
300
- };
301
- }
302
-
303
- const rootState = await getRecruitRoots(client);
304
- const closeTarget = await findVisibleCloseTarget(client, rootState.roots, RECRUIT_DETAIL_CLOSE_SELECTORS);
305
- if (closeTarget) {
306
- try {
307
- if (closeTarget.center) {
308
- await clickPoint(client, closeTarget.center.x, closeTarget.center.y);
309
- } else {
310
- await clickNodeCenter(client, closeTarget.node_id);
311
- }
312
- attempts.push({
313
- mode: "close-selector",
314
- selector: closeTarget.selector,
315
- root: closeTarget.root
316
- });
317
- } catch (error) {
318
- attempts.push({
319
- mode: "close-selector-error",
320
- selector: closeTarget.selector,
321
- root: closeTarget.root,
322
- error: error?.message || String(error)
323
- });
324
- await pressEscape(client);
325
- attempts.push({ mode: "Escape-after-close-selector-error" });
326
- }
327
- await sleep(700);
328
- } else {
329
- await pressEscape(client);
330
- attempts.push({ mode: "Escape" });
331
- await sleep(700);
332
- }
333
-
334
- let state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
335
- if (!state?.popup && !state?.resumeIframe) {
336
- return {
337
- closed: true,
338
- attempts
339
- };
340
- }
341
-
342
- await pressEscape(client);
343
- attempts.push({ mode: "Escape-fallback" });
344
- await sleep(700);
345
-
346
- state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
347
- if (!state?.popup && !state?.resumeIframe) {
348
- return {
349
- closed: true,
350
- attempts
351
- };
352
- }
353
- }
354
-
355
- return {
356
- closed: false,
357
- attempts
358
- };
359
- }
360
-
361
- async function findVisibleCloseTarget(client, roots, selectors) {
362
- let fallback = null;
363
- for (const root of roots) {
364
- if (!root?.nodeId) continue;
365
- for (const selector of selectors) {
366
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
367
- for (const nodeId of nodeIds) {
368
- const target = {
369
- root: root.name,
370
- root_node_id: root.nodeId,
371
- selector,
372
- node_id: nodeId
373
- };
374
- if (!fallback) fallback = target;
375
- try {
376
- const box = await getNodeBox(client, nodeId);
377
- if (box.rect.width > 2 && box.rect.height > 2) {
378
- return {
379
- ...target,
380
- center: box.center,
381
- rect: box.rect
382
- };
383
- }
384
- } catch {}
385
- }
386
- }
387
- }
388
- return fallback;
389
- }
390
-
391
- async function pressEscape(client) {
392
- await pressKey(client, "Escape", {
393
- code: "Escape",
394
- windowsVirtualKeyCode: 27,
395
- nativeVirtualKeyCode: 27
396
- });
397
- }
398
-
399
- export async function extractRecruitDetailCandidate(client, {
400
- cardCandidate,
401
- cardNodeId,
402
- detailState,
403
- detailHtml: providedDetailHtml = null,
404
- networkEvents = [],
405
- targetUrl = "",
406
- closeDetail = true,
407
- networkParseRetryMs = 1800,
408
- networkParseIntervalMs = 250
409
- } = {}) {
410
- const detailHtml = providedDetailHtml || await readRecruitDetailHtml(client, detailState);
411
- const detailText = [
412
- detailHtml.popupText,
413
- detailHtml.resumeText
414
- ].filter(Boolean).join("\n\n");
415
-
416
- const parseStarted = Date.now();
417
- let networkBodies = [];
418
- let detailCandidateResult = null;
419
- do {
420
- networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
421
- detailCandidateResult = buildScreeningCandidateFromDetail({
422
- domain: "recruit",
423
- cardCandidate,
424
- detailText,
425
- networkBodies,
426
- metadata: {
427
- target_url: targetUrl,
428
- card_node_id: cardNodeId,
429
- detail_popup_selector: detailState?.popup?.selector || null,
430
- detail_popup_root: detailState?.popup?.root || null,
431
- resume_iframe_selector: detailState?.resumeIframe?.selector || null,
432
- resume_iframe_root: detailState?.resumeIframe?.root || null,
433
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
434
- }
435
- });
436
- if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
437
- if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
438
- await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
439
- } while (true);
440
-
441
- let closeResult = null;
442
- if (closeDetail) {
443
- closeResult = await closeRecruitDetail(client);
444
- }
445
-
446
- return {
447
- candidate: detailCandidateResult.candidate,
448
- parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
449
- network_bodies: networkBodies,
450
- network_parse_retry_elapsed_ms: Date.now() - parseStarted,
451
- network_event_count: networkEvents.length,
452
- detail: {
453
- popup_text: detailHtml.popupText,
454
- resume_text: detailHtml.resumeText,
455
- popup_html_length: detailHtml.popupHTML.length,
456
- resume_html_length: detailHtml.resumeHTML.length
457
- },
458
- close_result: closeResult
459
- };
460
- }
1
+ import {
2
+ clickNodeCenter,
3
+ clickPoint,
4
+ DETERMINISTIC_CLICK_OPTIONS,
5
+ getFrameDocumentNodeId,
6
+ getNodeBox,
7
+ getOuterHTML,
8
+ pressKey,
9
+ querySelectorAll,
10
+ sleep
11
+ } from "../../core/browser/index.js";
12
+ import {
13
+ buildScreeningCandidateFromDetail,
14
+ htmlToText
15
+ } from "../../core/screening/index.js";
16
+ import {
17
+ RECRUIT_DETAIL_CLOSE_SELECTORS,
18
+ RECRUIT_DETAIL_NETWORK_PATTERNS,
19
+ RECRUIT_DETAIL_POPUP_SELECTORS,
20
+ RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS
21
+ } from "./constants.js";
22
+ import {
23
+ getRecruitRoots
24
+ } from "./roots.js";
25
+
26
+ export function matchesRecruitDetailNetwork(url) {
27
+ return RECRUIT_DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
28
+ }
29
+
30
+ export function createRecruitDetailNetworkRecorder(client) {
31
+ const events = [];
32
+ client.Network.responseReceived((event) => {
33
+ const url = event?.response?.url || "";
34
+ if (!matchesRecruitDetailNetwork(url)) return;
35
+ events.push({
36
+ requestId: event.requestId,
37
+ url,
38
+ status: event.response?.status,
39
+ mimeType: event.response?.mimeType,
40
+ type: event.type
41
+ });
42
+ });
43
+ if (typeof client.Network.loadingFinished === "function") {
44
+ client.Network.loadingFinished((event) => {
45
+ const found = events.find((item) => item.requestId === event.requestId);
46
+ if (!found) return;
47
+ found.loading_finished = true;
48
+ found.encodedDataLength = event.encodedDataLength;
49
+ });
50
+ }
51
+ if (typeof client.Network.loadingFailed === "function") {
52
+ client.Network.loadingFailed((event) => {
53
+ const found = events.find((item) => item.requestId === event.requestId);
54
+ if (!found) return;
55
+ found.loading_failed = true;
56
+ found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
57
+ });
58
+ }
59
+ return {
60
+ events,
61
+ clear() {
62
+ events.length = 0;
63
+ }
64
+ };
65
+ }
66
+
67
+ export async function waitForRecruitDetailNetworkEvents(recorder, {
68
+ minCount = 1,
69
+ requireLoaded = true,
70
+ timeoutMs = 3500,
71
+ intervalMs = 100
72
+ } = {}) {
73
+ const started = Date.now();
74
+ const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
75
+ let matching = [];
76
+ while (Date.now() - started <= timeoutMs) {
77
+ matching = events.filter((event) => (
78
+ !requireLoaded
79
+ || event.loading_finished === true
80
+ || event.loading_failed === true
81
+ ));
82
+ if (matching.length >= minCount) {
83
+ return {
84
+ ok: true,
85
+ elapsed_ms: Date.now() - started,
86
+ count: matching.length,
87
+ events: matching
88
+ };
89
+ }
90
+ await sleep(intervalMs);
91
+ }
92
+ return {
93
+ ok: false,
94
+ elapsed_ms: Date.now() - started,
95
+ count: matching.length,
96
+ events: matching,
97
+ total_event_count: events.length
98
+ };
99
+ }
100
+
101
+ export async function readRecruitDetailNetworkBodies(client, events = [], {
102
+ limit = 10
103
+ } = {}) {
104
+ const bodies = [];
105
+ for (const event of events.slice(0, limit)) {
106
+ try {
107
+ const body = await client.Network.getResponseBody({ requestId: event.requestId });
108
+ bodies.push({
109
+ ...event,
110
+ body,
111
+ body_length: String(body?.body || "").length
112
+ });
113
+ } catch (error) {
114
+ bodies.push({
115
+ ...event,
116
+ body_error: error?.message || String(error)
117
+ });
118
+ }
119
+ }
120
+ return bodies;
121
+ }
122
+
123
+ export async function waitForRecruitDetail(client, {
124
+ timeoutMs = 12000,
125
+ intervalMs = 250
126
+ } = {}) {
127
+ const started = Date.now();
128
+ let lastState = null;
129
+ while (Date.now() - started <= timeoutMs) {
130
+ const rootState = await getRecruitRoots(client);
131
+ const popup = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
132
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
133
+ lastState = {
134
+ iframe: rootState.iframe,
135
+ roots: rootState.roots,
136
+ popup,
137
+ resumeIframe
138
+ };
139
+ if (popup || resumeIframe) return lastState;
140
+ await sleep(intervalMs);
141
+ }
142
+ return lastState;
143
+ }
144
+
145
+ async function findVisibleDetailTarget(client, roots, selectors) {
146
+ for (const root of roots) {
147
+ if (!root?.nodeId) continue;
148
+ for (const selector of selectors) {
149
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
150
+ for (const nodeId of nodeIds) {
151
+ try {
152
+ const box = await getNodeBox(client, nodeId);
153
+ if (box.rect.width > 2 && box.rect.height > 2) {
154
+ return {
155
+ root: root.name,
156
+ root_node_id: root.nodeId,
157
+ selector,
158
+ node_id: nodeId,
159
+ center: box.center,
160
+ rect: box.rect
161
+ };
162
+ }
163
+ } catch {}
164
+ }
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+
170
+ export async function readRecruitDetailHtml(client, detailState) {
171
+ let popupHTML = "";
172
+ let resumeHTML = "";
173
+ let resumeIframeDocumentNodeId = null;
174
+
175
+ if (detailState?.popup?.node_id) {
176
+ popupHTML = await getOuterHTML(client, detailState.popup.node_id);
177
+ }
178
+
179
+ if (detailState?.resumeIframe?.node_id) {
180
+ resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
181
+ resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
182
+ }
183
+
184
+ return {
185
+ popupHTML,
186
+ resumeHTML,
187
+ resumeIframeDocumentNodeId,
188
+ popupText: htmlToText(popupHTML),
189
+ resumeText: htmlToText(resumeHTML)
190
+ };
191
+ }
192
+
193
+ export async function waitForRecruitDetailContent(client, {
194
+ minTextLength = 200,
195
+ timeoutMs = 6000,
196
+ intervalMs = 200
197
+ } = {}) {
198
+ const started = Date.now();
199
+ let lastState = null;
200
+ let lastHtml = null;
201
+ let lastError = null;
202
+ while (Date.now() - started <= timeoutMs) {
203
+ try {
204
+ lastState = await waitForRecruitDetail(client, {
205
+ timeoutMs: 500,
206
+ intervalMs: 100
207
+ });
208
+ if (lastState?.popup || lastState?.resumeIframe) {
209
+ lastHtml = await readRecruitDetailHtml(client, lastState);
210
+ const textLength = (lastHtml.popupText || "").length + (lastHtml.resumeText || "").length;
211
+ if (textLength >= minTextLength) {
212
+ return {
213
+ ok: true,
214
+ elapsed_ms: Date.now() - started,
215
+ text_length: textLength,
216
+ detail_state: lastState,
217
+ detail_html: lastHtml
218
+ };
219
+ }
220
+ }
221
+ } catch (error) {
222
+ lastError = error;
223
+ }
224
+ await sleep(intervalMs);
225
+ }
226
+
227
+ const textLength = (lastHtml?.popupText || "").length + (lastHtml?.resumeText || "").length;
228
+ return {
229
+ ok: false,
230
+ elapsed_ms: Date.now() - started,
231
+ text_length: textLength,
232
+ detail_state: lastState,
233
+ detail_html: lastHtml,
234
+ error: lastError?.message || null
235
+ };
236
+ }
237
+
238
+ export async function openRecruitCardDetail(client, cardNodeId, {
239
+ timeoutMs = 12000
240
+ } = {}) {
241
+ const openedStarted = Date.now();
242
+ const attempts = [];
243
+ const clickStarted = Date.now();
244
+ const cardBox = await clickNodeCenter(client, cardNodeId, {
245
+ scrollIntoView: true
246
+ });
247
+ let candidateClickMs = Date.now() - clickStarted;
248
+ attempts.push({
249
+ mode: "card-center",
250
+ center: cardBox.center
251
+ });
252
+ const detailStarted = Date.now();
253
+ let detailState = await waitForRecruitDetail(client, { timeoutMs });
254
+
255
+ if (!detailState?.popup && !detailState?.resumeIframe) {
256
+ const fallbackClickStarted = Date.now();
257
+ const leftTitlePoint = {
258
+ x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
259
+ y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
260
+ };
261
+ await clickPoint(client, leftTitlePoint.x, leftTitlePoint.y, {
262
+ clickCount: 2,
263
+ delayMs: 120
264
+ });
265
+ candidateClickMs += Date.now() - fallbackClickStarted;
266
+ attempts.push({
267
+ mode: "card-left-title-double-click",
268
+ center: leftTitlePoint
269
+ });
270
+ detailState = await waitForRecruitDetail(client, {
271
+ timeoutMs: Math.max(3000, Math.floor(timeoutMs / 2))
272
+ });
273
+ }
274
+
275
+ if (!detailState?.popup && !detailState?.resumeIframe) {
276
+ throw new Error("Recruit candidate detail did not open or no known detail selectors mounted");
277
+ }
278
+
279
+ return {
280
+ card_box: cardBox,
281
+ open_attempts: attempts,
282
+ detail_state: detailState,
283
+ timings: {
284
+ candidate_click_ms: candidateClickMs,
285
+ detail_open_ms: Date.now() - detailStarted,
286
+ open_total_ms: Date.now() - openedStarted
287
+ }
288
+ };
289
+ }
290
+
291
+ export async function closeRecruitDetail(client, {
292
+ attemptsLimit = 3
293
+ } = {}) {
294
+ const attempts = [];
295
+ for (let index = 0; index < attemptsLimit; index += 1) {
296
+ const existingState = await waitForRecruitDetail(client, { timeoutMs: 500 });
297
+ if (!existingState?.popup && !existingState?.resumeIframe) {
298
+ return {
299
+ closed: true,
300
+ attempts
301
+ };
302
+ }
303
+
304
+ const rootState = await getRecruitRoots(client);
305
+ const closeTarget = await findVisibleCloseTarget(client, rootState.roots, RECRUIT_DETAIL_CLOSE_SELECTORS);
306
+ if (closeTarget) {
307
+ try {
308
+ if (closeTarget.center) {
309
+ await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
310
+ } else {
311
+ await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
312
+ }
313
+ attempts.push({
314
+ mode: "close-selector",
315
+ selector: closeTarget.selector,
316
+ root: closeTarget.root
317
+ });
318
+ } catch (error) {
319
+ attempts.push({
320
+ mode: "close-selector-error",
321
+ selector: closeTarget.selector,
322
+ root: closeTarget.root,
323
+ error: error?.message || String(error)
324
+ });
325
+ await pressEscape(client);
326
+ attempts.push({ mode: "Escape-after-close-selector-error" });
327
+ }
328
+ await sleep(700);
329
+ } else {
330
+ await pressEscape(client);
331
+ attempts.push({ mode: "Escape" });
332
+ await sleep(700);
333
+ }
334
+
335
+ let state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
336
+ if (!state?.popup && !state?.resumeIframe) {
337
+ return {
338
+ closed: true,
339
+ attempts
340
+ };
341
+ }
342
+
343
+ await pressEscape(client);
344
+ attempts.push({ mode: "Escape-fallback" });
345
+ await sleep(700);
346
+
347
+ state = await waitForRecruitDetail(client, { timeoutMs: 1000 });
348
+ if (!state?.popup && !state?.resumeIframe) {
349
+ return {
350
+ closed: true,
351
+ attempts
352
+ };
353
+ }
354
+ }
355
+
356
+ return {
357
+ closed: false,
358
+ attempts
359
+ };
360
+ }
361
+
362
+ async function findVisibleCloseTarget(client, roots, selectors) {
363
+ let fallback = null;
364
+ for (const root of roots) {
365
+ if (!root?.nodeId) continue;
366
+ for (const selector of selectors) {
367
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
368
+ for (const nodeId of nodeIds) {
369
+ const target = {
370
+ root: root.name,
371
+ root_node_id: root.nodeId,
372
+ selector,
373
+ node_id: nodeId
374
+ };
375
+ if (!fallback) fallback = target;
376
+ try {
377
+ const box = await getNodeBox(client, nodeId);
378
+ if (box.rect.width > 2 && box.rect.height > 2) {
379
+ return {
380
+ ...target,
381
+ center: box.center,
382
+ rect: box.rect
383
+ };
384
+ }
385
+ } catch {}
386
+ }
387
+ }
388
+ }
389
+ return fallback;
390
+ }
391
+
392
+ async function pressEscape(client) {
393
+ await pressKey(client, "Escape", {
394
+ code: "Escape",
395
+ windowsVirtualKeyCode: 27,
396
+ nativeVirtualKeyCode: 27
397
+ });
398
+ }
399
+
400
+ export async function extractRecruitDetailCandidate(client, {
401
+ cardCandidate,
402
+ cardNodeId,
403
+ detailState,
404
+ detailHtml: providedDetailHtml = null,
405
+ networkEvents = [],
406
+ targetUrl = "",
407
+ closeDetail = true,
408
+ networkParseRetryMs = 1800,
409
+ networkParseIntervalMs = 250
410
+ } = {}) {
411
+ const detailHtml = providedDetailHtml || await readRecruitDetailHtml(client, detailState);
412
+ const detailText = [
413
+ detailHtml.popupText,
414
+ detailHtml.resumeText
415
+ ].filter(Boolean).join("\n\n");
416
+
417
+ const parseStarted = Date.now();
418
+ let networkBodies = [];
419
+ let detailCandidateResult = null;
420
+ do {
421
+ networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
422
+ detailCandidateResult = buildScreeningCandidateFromDetail({
423
+ domain: "recruit",
424
+ cardCandidate,
425
+ detailText,
426
+ networkBodies,
427
+ metadata: {
428
+ target_url: targetUrl,
429
+ card_node_id: cardNodeId,
430
+ detail_popup_selector: detailState?.popup?.selector || null,
431
+ detail_popup_root: detailState?.popup?.root || null,
432
+ resume_iframe_selector: detailState?.resumeIframe?.selector || null,
433
+ resume_iframe_root: detailState?.resumeIframe?.root || null,
434
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
435
+ }
436
+ });
437
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
438
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
439
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
440
+ } while (true);
441
+
442
+ let closeResult = null;
443
+ if (closeDetail) {
444
+ closeResult = await closeRecruitDetail(client);
445
+ }
446
+
447
+ return {
448
+ candidate: detailCandidateResult.candidate,
449
+ parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
450
+ network_bodies: networkBodies,
451
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
452
+ network_event_count: networkEvents.length,
453
+ detail: {
454
+ popup_text: detailHtml.popupText,
455
+ resume_text: detailHtml.resumeText,
456
+ popup_html_length: detailHtml.popupHTML.length,
457
+ resume_html_length: detailHtml.resumeHTML.length
458
+ },
459
+ close_result: closeResult
460
+ };
461
+ }