@reconcrap/boss-recommend-mcp 2.1.20 → 2.1.22
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.
- package/README.md +5 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +8 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2419 -2372
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +32 -32
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +529 -499
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +650 -650
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +504 -361
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +987 -854
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +13 -0
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +929 -915
- package/src/recommend-scheduler.js +496 -496
- package/src/recruit-mcp.js +2121 -2121
|
@@ -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
|
+
}
|