@reconcrap/boss-recommend-mcp 2.0.49 → 2.0.50
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/package.json +1 -1
- package/src/domains/chat/jobs.js +28 -0
- package/src/domains/chat/page-guard.js +25 -1
- package/src/domains/chat/run-service.js +4 -2
- package/src/domains/common/recovery-settle.js +159 -0
- package/src/domains/recommend/jobs.js +169 -5
- package/src/domains/recommend/refresh.js +68 -4
- package/src/domains/recommend/run-service.js +40 -9
- package/src/domains/recruit/refresh.js +1 -0
- package/src/domains/recruit/run-service.js +8 -0
- package/src/domains/recruit/search.js +52 -0
package/package.json
CHANGED
package/src/domains/chat/jobs.js
CHANGED
|
@@ -443,6 +443,34 @@ export async function closeChatJobDropdown(client, rootNodeId, {
|
|
|
443
443
|
});
|
|
444
444
|
if (settleMs > 0) await sleep(settleMs);
|
|
445
445
|
const after = await visibleChatJobOptions(client, rootNodeId);
|
|
446
|
+
if (after.length) {
|
|
447
|
+
const currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
448
|
+
for (const selector of CHAT_JOB_TRIGGER_SELECTORS) {
|
|
449
|
+
const nodeIds = await safeQuerySelectorAll(client, currentRootNodeId, selector);
|
|
450
|
+
for (const nodeId of nodeIds) {
|
|
451
|
+
try {
|
|
452
|
+
const box = await getNodeBox(client, nodeId);
|
|
453
|
+
if (box.rect.width <= 2 || box.rect.height <= 2) continue;
|
|
454
|
+
await clickPoint(client, box.center.x, box.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
455
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
456
|
+
const afterToggle = await visibleChatJobOptions(client, currentRootNodeId);
|
|
457
|
+
if (!afterToggle.length) {
|
|
458
|
+
return {
|
|
459
|
+
ok: true,
|
|
460
|
+
closed: true,
|
|
461
|
+
reason: "trigger_toggle",
|
|
462
|
+
visible_before_count: before.length,
|
|
463
|
+
visible_after_count: 0,
|
|
464
|
+
first_visible_before: before[0] || null,
|
|
465
|
+
first_visible_after: null
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
446
474
|
return {
|
|
447
475
|
ok: after.length === 0,
|
|
448
476
|
closed: after.length === 0,
|
|
@@ -3,6 +3,14 @@ import {
|
|
|
3
3
|
sleep,
|
|
4
4
|
waitForMainFrameUrl
|
|
5
5
|
} from "../../core/browser/index.js";
|
|
6
|
+
import {
|
|
7
|
+
buildChatSelfHealConfig,
|
|
8
|
+
resolveChatSelfHealRoots
|
|
9
|
+
} from "../../core/self-heal/index.js";
|
|
10
|
+
import {
|
|
11
|
+
createRecoverySettleError,
|
|
12
|
+
waitForMiniFreshStartSettle
|
|
13
|
+
} from "../common/recovery-settle.js";
|
|
6
14
|
import { CHAT_TARGET_URL } from "./constants.js";
|
|
7
15
|
|
|
8
16
|
export const CHAT_FORBIDDEN_TOP_LEVEL_RESUME_CODE = "CHAT_FORBIDDEN_TOP_LEVEL_RESUME_NAVIGATION";
|
|
@@ -63,7 +71,8 @@ export async function recoverChatShell(client, {
|
|
|
63
71
|
timeoutMs = 60000,
|
|
64
72
|
intervalMs = 500,
|
|
65
73
|
forceNavigate = false,
|
|
66
|
-
settleMs = 1200
|
|
74
|
+
settleMs = 1200,
|
|
75
|
+
settleAfterNavigate = false
|
|
67
76
|
} = {}) {
|
|
68
77
|
const before = await getChatTopLevelState(client);
|
|
69
78
|
if (before.is_chat_shell && !forceNavigate) {
|
|
@@ -85,11 +94,26 @@ export async function recoverChatShell(client, {
|
|
|
85
94
|
intervalMs
|
|
86
95
|
});
|
|
87
96
|
const after = await getChatTopLevelState(client);
|
|
97
|
+
let miniFreshStart = null;
|
|
98
|
+
if (after.is_chat_shell && settleAfterNavigate) {
|
|
99
|
+
miniFreshStart = await waitForMiniFreshStartSettle(client, {
|
|
100
|
+
domain: "chat",
|
|
101
|
+
timeoutMs,
|
|
102
|
+
intervalMs: Math.max(intervalMs, 800),
|
|
103
|
+
settleMs: Math.min(settleMs, 5000),
|
|
104
|
+
selfHealConfig: buildChatSelfHealConfig(),
|
|
105
|
+
resolveSelfHealRoots: resolveChatSelfHealRoots
|
|
106
|
+
});
|
|
107
|
+
if (!miniFreshStart.ok) {
|
|
108
|
+
throw createRecoverySettleError("chat", miniFreshStart);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
88
111
|
return {
|
|
89
112
|
recovered: waited.ok && after.is_chat_shell,
|
|
90
113
|
refreshed: Boolean(forceNavigate && before.is_chat_shell && after.is_chat_shell),
|
|
91
114
|
before,
|
|
92
115
|
after,
|
|
116
|
+
mini_fresh_start: miniFreshStart,
|
|
93
117
|
wait: waited,
|
|
94
118
|
navigate_result: navigateResult || null,
|
|
95
119
|
navigate_url: targetUrl,
|
|
@@ -788,7 +788,8 @@ export async function runChatWorkflow({
|
|
|
788
788
|
if (!initialTopLevelState.is_chat_shell) {
|
|
789
789
|
const recovery = await recoverChatShell(client, {
|
|
790
790
|
targetUrl,
|
|
791
|
-
timeoutMs: readyTimeoutMs
|
|
791
|
+
timeoutMs: readyTimeoutMs,
|
|
792
|
+
settleAfterNavigate: true
|
|
792
793
|
});
|
|
793
794
|
runControl.checkpoint({
|
|
794
795
|
chat_shell_recovery: {
|
|
@@ -831,7 +832,8 @@ export async function runChatWorkflow({
|
|
|
831
832
|
const shellRecovery = await recoverChatShell(client, {
|
|
832
833
|
targetUrl,
|
|
833
834
|
timeoutMs: readyTimeoutMs,
|
|
834
|
-
forceNavigate: forceRefresh
|
|
835
|
+
forceNavigate: forceRefresh,
|
|
836
|
+
settleAfterNavigate: true
|
|
835
837
|
});
|
|
836
838
|
runControl.checkpoint({
|
|
837
839
|
chat_shell_recovery: {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bringPageToFront,
|
|
3
|
+
detectBossLoginState,
|
|
4
|
+
sleep
|
|
5
|
+
} from "../../core/browser/index.js";
|
|
6
|
+
import {
|
|
7
|
+
HEALTH_STATUS,
|
|
8
|
+
runSelfHealCheck
|
|
9
|
+
} from "../../core/self-heal/index.js";
|
|
10
|
+
|
|
11
|
+
function compactProbe(probe = {}) {
|
|
12
|
+
return {
|
|
13
|
+
id: probe.id || "",
|
|
14
|
+
type: probe.type || "",
|
|
15
|
+
status: probe.status || "",
|
|
16
|
+
count: probe.count || 0,
|
|
17
|
+
required: Boolean(probe.required),
|
|
18
|
+
error: probe.error || null
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function compactRecoveryHealth(check = null) {
|
|
23
|
+
if (!check) return null;
|
|
24
|
+
return {
|
|
25
|
+
domain: check.domain || "",
|
|
26
|
+
status: check.status || "",
|
|
27
|
+
summary: check.summary || null,
|
|
28
|
+
drift_report: check.drift_report || null,
|
|
29
|
+
probes: (check.probes || []).map(compactProbe)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createRecoverySettleError(domain, settle = {}) {
|
|
34
|
+
const status = settle.status || settle.reason || "unknown";
|
|
35
|
+
const error = new Error(`${domain} mini fresh-start settle failed: ${status}`);
|
|
36
|
+
error.code = `${String(domain || "boss").toUpperCase()}_RECOVERY_SETTLE_FAILED`;
|
|
37
|
+
error.recovery_settle = settle;
|
|
38
|
+
error.retryable = true;
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function waitForMiniFreshStartSettle(client, {
|
|
43
|
+
domain = "boss",
|
|
44
|
+
timeoutMs = 90000,
|
|
45
|
+
intervalMs = 800,
|
|
46
|
+
settleMs = 0,
|
|
47
|
+
readinessLabel = "ready",
|
|
48
|
+
checkReady = null,
|
|
49
|
+
selfHealConfig = null,
|
|
50
|
+
resolveSelfHealRoots = null
|
|
51
|
+
} = {}) {
|
|
52
|
+
const started = Date.now();
|
|
53
|
+
let lastReady = null;
|
|
54
|
+
let lastHealth = null;
|
|
55
|
+
let lastRoots = null;
|
|
56
|
+
let lastLoginDetection = null;
|
|
57
|
+
|
|
58
|
+
if (typeof client?.Network?.setCacheDisabled === "function") {
|
|
59
|
+
await client.Network.setCacheDisabled({ cacheDisabled: true }).catch(() => null);
|
|
60
|
+
}
|
|
61
|
+
await bringPageToFront(client).catch(() => null);
|
|
62
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
63
|
+
|
|
64
|
+
while (Date.now() - started <= timeoutMs) {
|
|
65
|
+
lastLoginDetection = await detectBossLoginState(client).catch((error) => ({
|
|
66
|
+
requires_login: false,
|
|
67
|
+
reason: "login_detection_failed",
|
|
68
|
+
error: error?.message || String(error || "")
|
|
69
|
+
}));
|
|
70
|
+
if (lastLoginDetection?.requires_login) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
domain,
|
|
74
|
+
status: "login_required",
|
|
75
|
+
reason: "login_required",
|
|
76
|
+
elapsed_ms: Date.now() - started,
|
|
77
|
+
login_detection: lastLoginDetection,
|
|
78
|
+
readiness: lastReady,
|
|
79
|
+
health: compactRecoveryHealth(lastHealth),
|
|
80
|
+
roots: lastRoots
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof checkReady === "function") {
|
|
85
|
+
lastReady = await checkReady({
|
|
86
|
+
elapsedMs: Date.now() - started,
|
|
87
|
+
remainingMs: Math.max(1, timeoutMs - (Date.now() - started))
|
|
88
|
+
}).catch((error) => ({
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: "readiness_check_failed",
|
|
91
|
+
error: error?.message || String(error || "")
|
|
92
|
+
}));
|
|
93
|
+
if (lastReady?.ok) {
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
domain,
|
|
97
|
+
status: "ready",
|
|
98
|
+
reason: readinessLabel,
|
|
99
|
+
elapsed_ms: Date.now() - started,
|
|
100
|
+
readiness: lastReady,
|
|
101
|
+
health: compactRecoveryHealth(lastHealth),
|
|
102
|
+
roots: lastRoots
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (selfHealConfig && typeof resolveSelfHealRoots === "function") {
|
|
108
|
+
const rootsResult = await resolveSelfHealRoots(client, selfHealConfig).catch((error) => ({
|
|
109
|
+
roots: {},
|
|
110
|
+
error: error?.message || String(error || "")
|
|
111
|
+
}));
|
|
112
|
+
lastRoots = rootsResult?.roots || {};
|
|
113
|
+
lastHealth = await runSelfHealCheck({
|
|
114
|
+
client,
|
|
115
|
+
domain,
|
|
116
|
+
roots: lastRoots,
|
|
117
|
+
selectorProbes: selfHealConfig.selectorProbes || [],
|
|
118
|
+
accessibilityProbes: selfHealConfig.accessibilityProbes || [],
|
|
119
|
+
viewportProbes: selfHealConfig.viewportProbes || []
|
|
120
|
+
}).catch((error) => ({
|
|
121
|
+
domain,
|
|
122
|
+
status: "failed",
|
|
123
|
+
summary: {
|
|
124
|
+
status: "failed",
|
|
125
|
+
failed_required: 1
|
|
126
|
+
},
|
|
127
|
+
probes: [],
|
|
128
|
+
drift_report: [],
|
|
129
|
+
error: error?.message || String(error || "")
|
|
130
|
+
}));
|
|
131
|
+
if (lastHealth?.status === HEALTH_STATUS.HEALTHY) {
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
domain,
|
|
135
|
+
status: HEALTH_STATUS.HEALTHY,
|
|
136
|
+
reason: "self_heal_healthy",
|
|
137
|
+
elapsed_ms: Date.now() - started,
|
|
138
|
+
readiness: lastReady,
|
|
139
|
+
health: compactRecoveryHealth(lastHealth),
|
|
140
|
+
roots: lastRoots
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await sleep(intervalMs);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
domain,
|
|
151
|
+
status: lastHealth?.status || lastReady?.reason || "timeout",
|
|
152
|
+
reason: "timeout",
|
|
153
|
+
elapsed_ms: Date.now() - started,
|
|
154
|
+
login_detection: lastLoginDetection,
|
|
155
|
+
readiness: lastReady,
|
|
156
|
+
health: compactRecoveryHealth(lastHealth),
|
|
157
|
+
roots: lastRoots
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clickNodeCenter,
|
|
3
|
+
clickPoint,
|
|
3
4
|
DETERMINISTIC_CLICK_OPTIONS,
|
|
4
5
|
getAttributesMap,
|
|
5
6
|
getNodeBox,
|
|
@@ -265,6 +266,150 @@ export async function closeRecommendJobDropdown(client) {
|
|
|
265
266
|
};
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
async function readVisibleRecommendJobOptions(client, frameNodeId) {
|
|
270
|
+
const options = await listRecommendJobOptions(client, frameNodeId, {
|
|
271
|
+
openDropdown: false
|
|
272
|
+
}).catch(() => []);
|
|
273
|
+
return {
|
|
274
|
+
options,
|
|
275
|
+
visible_options: options.filter((option) => option.visible)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function closeRecommendJobDropdownFully(client, frameNodeId, {
|
|
280
|
+
settleMs = 300,
|
|
281
|
+
timeoutMs = 1200
|
|
282
|
+
} = {}) {
|
|
283
|
+
const before = await readVisibleRecommendJobOptions(client, frameNodeId);
|
|
284
|
+
const attempts = [];
|
|
285
|
+
if (!before.visible_options.length) {
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
closed: false,
|
|
289
|
+
reason: "already_closed",
|
|
290
|
+
visible_before_count: 0,
|
|
291
|
+
visible_after_count: 0,
|
|
292
|
+
attempts
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const started = Date.now();
|
|
297
|
+
for (let attempt = 1; attempt <= 2 && Date.now() - started <= timeoutMs; attempt += 1) {
|
|
298
|
+
const close = await closeRecommendJobDropdown(client);
|
|
299
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
300
|
+
const afterEscape = await readVisibleRecommendJobOptions(client, frameNodeId);
|
|
301
|
+
attempts.push({
|
|
302
|
+
method: "escape",
|
|
303
|
+
attempt,
|
|
304
|
+
ok: afterEscape.visible_options.length === 0,
|
|
305
|
+
visible_after_count: afterEscape.visible_options.length,
|
|
306
|
+
close
|
|
307
|
+
});
|
|
308
|
+
if (!afterEscape.visible_options.length) {
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
closed: true,
|
|
312
|
+
reason: "escape",
|
|
313
|
+
visible_before_count: before.visible_options.length,
|
|
314
|
+
visible_after_count: 0,
|
|
315
|
+
attempts
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const trigger = await findRecommendJobTrigger(client, frameNodeId).catch(() => null);
|
|
321
|
+
if (trigger?.node_id) {
|
|
322
|
+
const click = await clickNodeCenter(client, trigger.node_id, DETERMINISTIC_CLICK_OPTIONS).catch((error) => ({
|
|
323
|
+
error: error?.message || String(error || "")
|
|
324
|
+
}));
|
|
325
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
326
|
+
const afterToggle = await readVisibleRecommendJobOptions(client, frameNodeId);
|
|
327
|
+
attempts.push({
|
|
328
|
+
method: "trigger_toggle",
|
|
329
|
+
ok: afterToggle.visible_options.length === 0,
|
|
330
|
+
visible_after_count: afterToggle.visible_options.length,
|
|
331
|
+
click
|
|
332
|
+
});
|
|
333
|
+
if (!afterToggle.visible_options.length) {
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
closed: true,
|
|
337
|
+
reason: "trigger_toggle",
|
|
338
|
+
visible_before_count: before.visible_options.length,
|
|
339
|
+
visible_after_count: 0,
|
|
340
|
+
attempts
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const outside = await clickPoint(client, 12, 12, DETERMINISTIC_CLICK_OPTIONS).catch((error) => ({
|
|
346
|
+
error: error?.message || String(error || "")
|
|
347
|
+
}));
|
|
348
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
349
|
+
const afterOutside = await readVisibleRecommendJobOptions(client, frameNodeId);
|
|
350
|
+
attempts.push({
|
|
351
|
+
method: "outside_click",
|
|
352
|
+
ok: afterOutside.visible_options.length === 0,
|
|
353
|
+
visible_after_count: afterOutside.visible_options.length,
|
|
354
|
+
click: outside
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
ok: afterOutside.visible_options.length === 0,
|
|
359
|
+
closed: afterOutside.visible_options.length === 0,
|
|
360
|
+
reason: afterOutside.visible_options.length ? "still_visible_after_close_attempts" : "outside_click",
|
|
361
|
+
visible_before_count: before.visible_options.length,
|
|
362
|
+
visible_after_count: afterOutside.visible_options.length,
|
|
363
|
+
attempts,
|
|
364
|
+
first_visible_after: afterOutside.visible_options[0] ? compactJobOption(afterOutside.visible_options[0]) : null
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function verifyRecommendJobSelection(client, frameNodeId, {
|
|
369
|
+
jobLabel = "",
|
|
370
|
+
delayMs = 2000,
|
|
371
|
+
dropdownTimeoutMs = 4000,
|
|
372
|
+
closeSettleMs = 300
|
|
373
|
+
} = {}) {
|
|
374
|
+
const requested = normalizeText(jobLabel);
|
|
375
|
+
if (delayMs > 0) await sleep(delayMs);
|
|
376
|
+
let options = [];
|
|
377
|
+
let openError = null;
|
|
378
|
+
try {
|
|
379
|
+
options = await listRecommendJobOptions(client, frameNodeId, {
|
|
380
|
+
openDropdown: true
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
openError = error;
|
|
384
|
+
options = await listRecommendJobOptions(client, frameNodeId, {
|
|
385
|
+
openDropdown: false
|
|
386
|
+
}).catch(() => []);
|
|
387
|
+
}
|
|
388
|
+
const current = options.find((option) => option.current) || null;
|
|
389
|
+
const verified = Boolean(current && jobLabelMatches(current.label, requested));
|
|
390
|
+
const menuClose = await closeRecommendJobDropdownFully(client, frameNodeId, {
|
|
391
|
+
settleMs: closeSettleMs,
|
|
392
|
+
timeoutMs: Math.max(1200, Math.min(4000, dropdownTimeoutMs))
|
|
393
|
+
}).catch((error) => ({
|
|
394
|
+
ok: false,
|
|
395
|
+
closed: false,
|
|
396
|
+
reason: "close_failed",
|
|
397
|
+
error: error?.message || String(error || "")
|
|
398
|
+
}));
|
|
399
|
+
return {
|
|
400
|
+
verified,
|
|
401
|
+
requested,
|
|
402
|
+
current_label: current?.label || "",
|
|
403
|
+
current_label_without_salary: current?.label_without_salary || "",
|
|
404
|
+
current_option: current ? compactJobOption(current) : null,
|
|
405
|
+
option_count: options.length,
|
|
406
|
+
visible_option_count: options.filter((option) => option.visible).length,
|
|
407
|
+
options: options.map(compactJobOption),
|
|
408
|
+
open_error: openError ? (openError?.message || String(openError)) : null,
|
|
409
|
+
menu_close: menuClose
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
268
413
|
export async function selectRecommendJob(client, frameNodeId, {
|
|
269
414
|
jobLabel = "",
|
|
270
415
|
settleMs = 6000,
|
|
@@ -294,7 +439,12 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
294
439
|
option.current && jobLabelMatches(option.label, target)
|
|
295
440
|
));
|
|
296
441
|
if (currentMatch) {
|
|
297
|
-
await
|
|
442
|
+
const menuClose = await closeRecommendJobDropdownFully(client, frameNodeId).catch((closeError) => ({
|
|
443
|
+
ok: false,
|
|
444
|
+
closed: false,
|
|
445
|
+
reason: "close_failed",
|
|
446
|
+
error: closeError?.message || String(closeError || "")
|
|
447
|
+
}));
|
|
298
448
|
return {
|
|
299
449
|
requested: target,
|
|
300
450
|
selected: true,
|
|
@@ -305,7 +455,8 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
305
455
|
}),
|
|
306
456
|
options: currentOptions.map(compactJobOption),
|
|
307
457
|
dropdown_error: error?.message || String(error),
|
|
308
|
-
job_dropdown_attempts: error?.job_dropdown_attempts || []
|
|
458
|
+
job_dropdown_attempts: error?.job_dropdown_attempts || [],
|
|
459
|
+
menu_close: menuClose
|
|
309
460
|
};
|
|
310
461
|
}
|
|
311
462
|
throw error;
|
|
@@ -333,13 +484,19 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
333
484
|
}
|
|
334
485
|
|
|
335
486
|
if (match.current) {
|
|
336
|
-
await
|
|
487
|
+
const menuClose = await closeRecommendJobDropdownFully(client, frameNodeId).catch((error) => ({
|
|
488
|
+
ok: false,
|
|
489
|
+
closed: false,
|
|
490
|
+
reason: "close_failed",
|
|
491
|
+
error: error?.message || String(error || "")
|
|
492
|
+
}));
|
|
337
493
|
return {
|
|
338
494
|
requested: target,
|
|
339
495
|
selected: true,
|
|
340
496
|
already_current: true,
|
|
341
497
|
selected_option: compactJobOption(match),
|
|
342
|
-
options: options.map(compactJobOption)
|
|
498
|
+
options: options.map(compactJobOption),
|
|
499
|
+
menu_close: menuClose
|
|
343
500
|
};
|
|
344
501
|
}
|
|
345
502
|
|
|
@@ -350,6 +507,12 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
350
507
|
|
|
351
508
|
const clickedBox = await clickNodeCenter(client, match.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
352
509
|
if (settleMs > 0) await sleep(settleMs);
|
|
510
|
+
const menuClose = await closeRecommendJobDropdownFully(client, frameNodeId).catch((error) => ({
|
|
511
|
+
ok: false,
|
|
512
|
+
closed: false,
|
|
513
|
+
reason: "close_failed",
|
|
514
|
+
error: error?.message || String(error || "")
|
|
515
|
+
}));
|
|
353
516
|
return {
|
|
354
517
|
requested: target,
|
|
355
518
|
selected: true,
|
|
@@ -359,7 +522,8 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
359
522
|
center: clickedBox.center,
|
|
360
523
|
rect: clickedBox.rect
|
|
361
524
|
},
|
|
362
|
-
options: options.map(compactJobOption)
|
|
525
|
+
options: options.map(compactJobOption),
|
|
526
|
+
menu_close: menuClose
|
|
363
527
|
};
|
|
364
528
|
}
|
|
365
529
|
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { sleep } from "../../core/browser/index.js";
|
|
2
|
+
import {
|
|
3
|
+
buildRecommendSelfHealConfig,
|
|
4
|
+
resolveRecommendSelfHealRoots
|
|
5
|
+
} from "../../core/self-heal/index.js";
|
|
6
|
+
import {
|
|
7
|
+
createRecoverySettleError,
|
|
8
|
+
waitForMiniFreshStartSettle
|
|
9
|
+
} from "../common/recovery-settle.js";
|
|
2
10
|
import {
|
|
3
11
|
clickRecommendEndRefreshButton,
|
|
4
12
|
waitForRecommendCardNodeIds
|
|
@@ -8,7 +16,10 @@ import {
|
|
|
8
16
|
RECOMMEND_TARGET_URL
|
|
9
17
|
} from "./constants.js";
|
|
10
18
|
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
11
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
selectRecommendJob,
|
|
21
|
+
verifyRecommendJobSelection
|
|
22
|
+
} from "./jobs.js";
|
|
12
23
|
import { selectRecommendPageScope } from "./scopes.js";
|
|
13
24
|
import {
|
|
14
25
|
getRecommendRoots,
|
|
@@ -105,7 +116,7 @@ function compactFilterReapplyError(error) {
|
|
|
105
116
|
export function isRetryableRecommendJobSelectionError(error) {
|
|
106
117
|
if (isStaleRecommendNodeError(error)) return true;
|
|
107
118
|
const message = String(error?.message || error || "");
|
|
108
|
-
return /Recommend job trigger was not found|Recommend job dropdown did not mount options|Recommend job dropdown did not expose visible options|Matched recommend job has no clickable center|Matched recommend job has no visible clickable option/i.test(message);
|
|
119
|
+
return /Recommend job trigger was not found|Recommend job dropdown did not mount options|Recommend job dropdown did not expose visible options|Matched recommend job has no clickable center|Matched recommend job has no visible clickable option|Recommend job selection was not sticky|Recommend job dropdown remained open after sticky verification/i.test(message);
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
function compactJobSelectionAttempt({
|
|
@@ -123,10 +134,29 @@ function compactJobSelectionAttempt({
|
|
|
123
134
|
attempt,
|
|
124
135
|
iframe_document_node_id: iframeDocumentNodeId || 0,
|
|
125
136
|
selected: Boolean(selection?.selected),
|
|
126
|
-
selection_reason: selection?.reason || null
|
|
137
|
+
selection_reason: selection?.reason || null,
|
|
138
|
+
sticky_verified: selection?.sticky_verification?.verified ?? null,
|
|
139
|
+
sticky_current_label: selection?.sticky_verification?.current_label_without_salary
|
|
140
|
+
|| selection?.sticky_verification?.current_label
|
|
141
|
+
|| null,
|
|
142
|
+
sticky_menu_closed: selection?.sticky_verification?.menu_close?.ok ?? null
|
|
127
143
|
};
|
|
128
144
|
}
|
|
129
145
|
|
|
146
|
+
async function waitForRecommendRecoverySettle(client, {
|
|
147
|
+
reloadSettleMs = 8000,
|
|
148
|
+
timeoutMs = 90000
|
|
149
|
+
} = {}) {
|
|
150
|
+
return waitForMiniFreshStartSettle(client, {
|
|
151
|
+
domain: "recommend",
|
|
152
|
+
timeoutMs,
|
|
153
|
+
intervalMs: reloadSettleMs > 10000 ? 1200 : 800,
|
|
154
|
+
settleMs: Math.max(0, Math.min(reloadSettleMs || 0, 5000)),
|
|
155
|
+
selfHealConfig: buildRecommendSelfHealConfig(),
|
|
156
|
+
resolveSelfHealRoots: resolveRecommendSelfHealRoots
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
130
160
|
async function waitForFreshRecommendRoots(client, {
|
|
131
161
|
timeoutMs = 10000,
|
|
132
162
|
intervalMs = 500
|
|
@@ -166,6 +196,31 @@ export async function selectRecommendJobWithRootRefresh(client, rootState, {
|
|
|
166
196
|
settleMs,
|
|
167
197
|
dropdownTimeoutMs
|
|
168
198
|
});
|
|
199
|
+
if (selection.selected) {
|
|
200
|
+
const stickyRootState = await waitForFreshRecommendRoots(client, {
|
|
201
|
+
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
202
|
+
intervalMs: 500
|
|
203
|
+
}) || currentRootState;
|
|
204
|
+
const stickyFrameNodeId = stickyRootState?.iframe?.documentNodeId || iframeDocumentNodeId;
|
|
205
|
+
const stickyVerification = await verifyRecommendJobSelection(client, stickyFrameNodeId, {
|
|
206
|
+
jobLabel,
|
|
207
|
+
delayMs: 2000,
|
|
208
|
+
dropdownTimeoutMs,
|
|
209
|
+
closeSettleMs: 300
|
|
210
|
+
});
|
|
211
|
+
selection.sticky_verification = stickyVerification;
|
|
212
|
+
currentRootState = stickyRootState || currentRootState;
|
|
213
|
+
if (!stickyVerification.verified) {
|
|
214
|
+
const stickyError = new Error(`Recommend job selection was not sticky after 2s: requested=${jobLabel}; current=${stickyVerification.current_label_without_salary || stickyVerification.current_label || "unknown"}`);
|
|
215
|
+
stickyError.sticky_verification = stickyVerification;
|
|
216
|
+
throw stickyError;
|
|
217
|
+
}
|
|
218
|
+
if (stickyVerification.menu_close && stickyVerification.menu_close.ok === false) {
|
|
219
|
+
const closeError = new Error(`Recommend job dropdown remained open after sticky verification: ${stickyVerification.menu_close.reason || "unknown"}`);
|
|
220
|
+
closeError.sticky_verification = stickyVerification;
|
|
221
|
+
throw closeError;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
169
224
|
attempts.push(compactJobSelectionAttempt({
|
|
170
225
|
ok: true,
|
|
171
226
|
attempt,
|
|
@@ -270,13 +325,20 @@ async function applyRefreshMethod(client, method, {
|
|
|
270
325
|
let pageScopeResult = null;
|
|
271
326
|
let filterResult = null;
|
|
272
327
|
let filterReapplyAttempts = [];
|
|
328
|
+
let recoverySettle = null;
|
|
273
329
|
try {
|
|
274
330
|
if (method === "page_navigate") {
|
|
275
331
|
await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
|
|
276
332
|
} else {
|
|
277
333
|
await client.Page.reload({ ignoreCache: true });
|
|
278
334
|
}
|
|
279
|
-
|
|
335
|
+
recoverySettle = await waitForRecommendRecoverySettle(client, {
|
|
336
|
+
reloadSettleMs,
|
|
337
|
+
timeoutMs: Math.max(45000, reloadSettleMs * 6)
|
|
338
|
+
});
|
|
339
|
+
if (!recoverySettle.ok) {
|
|
340
|
+
throw createRecoverySettleError("recommend", recoverySettle);
|
|
341
|
+
}
|
|
280
342
|
currentRootState = await waitForRecommendRoots(client, {
|
|
281
343
|
timeoutMs: Math.max(45000, reloadSettleMs * 6),
|
|
282
344
|
intervalMs: 500
|
|
@@ -337,6 +399,7 @@ async function applyRefreshMethod(client, method, {
|
|
|
337
399
|
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
338
400
|
job_selection: jobSelection,
|
|
339
401
|
job_selection_attempts: jobSelectionAttempts,
|
|
402
|
+
recovery_settle: recoverySettle,
|
|
340
403
|
page_scope: pageScopeResult,
|
|
341
404
|
filter: filterResult,
|
|
342
405
|
filter_reapply_attempts: filterReapplyAttempts,
|
|
@@ -354,6 +417,7 @@ async function applyRefreshMethod(client, method, {
|
|
|
354
417
|
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
355
418
|
job_selection: jobSelection,
|
|
356
419
|
job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
|
|
420
|
+
recovery_settle: error?.recovery_settle || recoverySettle,
|
|
357
421
|
page_scope: pageScopeResult,
|
|
358
422
|
filter: filterResult,
|
|
359
423
|
filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
|
|
@@ -144,10 +144,33 @@ function compactJobSelection(jobSelection) {
|
|
|
144
144
|
return {
|
|
145
145
|
requested: jobSelection.requested || "",
|
|
146
146
|
selected: Boolean(jobSelection.selected),
|
|
147
|
-
already_current: Boolean(jobSelection.already_current),
|
|
148
|
-
reason: jobSelection.reason || null,
|
|
149
|
-
selected_option: jobSelection.selected_option || null,
|
|
150
|
-
|
|
147
|
+
already_current: Boolean(jobSelection.already_current),
|
|
148
|
+
reason: jobSelection.reason || null,
|
|
149
|
+
selected_option: jobSelection.selected_option || null,
|
|
150
|
+
menu_close: jobSelection.menu_close
|
|
151
|
+
? {
|
|
152
|
+
ok: Boolean(jobSelection.menu_close.ok),
|
|
153
|
+
closed: Boolean(jobSelection.menu_close.closed),
|
|
154
|
+
reason: jobSelection.menu_close.reason || ""
|
|
155
|
+
}
|
|
156
|
+
: null,
|
|
157
|
+
sticky_verification: jobSelection.sticky_verification
|
|
158
|
+
? {
|
|
159
|
+
verified: Boolean(jobSelection.sticky_verification.verified),
|
|
160
|
+
current_label: jobSelection.sticky_verification.current_label_without_salary
|
|
161
|
+
|| jobSelection.sticky_verification.current_label
|
|
162
|
+
|| "",
|
|
163
|
+
visible_option_count: jobSelection.sticky_verification.visible_option_count || 0,
|
|
164
|
+
menu_close: jobSelection.sticky_verification.menu_close
|
|
165
|
+
? {
|
|
166
|
+
ok: Boolean(jobSelection.sticky_verification.menu_close.ok),
|
|
167
|
+
closed: Boolean(jobSelection.sticky_verification.menu_close.closed),
|
|
168
|
+
reason: jobSelection.sticky_verification.menu_close.reason || ""
|
|
169
|
+
}
|
|
170
|
+
: null
|
|
171
|
+
}
|
|
172
|
+
: null,
|
|
173
|
+
options: (jobSelection.options || []).map((option) => ({
|
|
151
174
|
label: option.label,
|
|
152
175
|
label_without_salary: option.label_without_salary,
|
|
153
176
|
current: Boolean(option.current),
|
|
@@ -364,11 +387,19 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
364
387
|
method: refreshAttempt.method || "",
|
|
365
388
|
reason: refreshAttempt.reason || null,
|
|
366
389
|
error: refreshAttempt.error || null,
|
|
367
|
-
forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
|
|
368
|
-
target_url: refreshAttempt.target_url || null,
|
|
369
|
-
card_count: refreshAttempt.card_count || 0,
|
|
370
|
-
elapsed_ms: refreshAttempt.elapsed_ms || 0,
|
|
371
|
-
|
|
390
|
+
forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
|
|
391
|
+
target_url: refreshAttempt.target_url || null,
|
|
392
|
+
card_count: refreshAttempt.card_count || 0,
|
|
393
|
+
elapsed_ms: refreshAttempt.elapsed_ms || 0,
|
|
394
|
+
recovery_settle: refreshAttempt.recovery_settle
|
|
395
|
+
? {
|
|
396
|
+
ok: Boolean(refreshAttempt.recovery_settle.ok),
|
|
397
|
+
status: refreshAttempt.recovery_settle.status || "",
|
|
398
|
+
reason: refreshAttempt.recovery_settle.reason || "",
|
|
399
|
+
elapsed_ms: refreshAttempt.recovery_settle.elapsed_ms || 0
|
|
400
|
+
}
|
|
401
|
+
: null,
|
|
402
|
+
attempts: (refreshAttempt.attempts || []).map((attempt) => ({
|
|
372
403
|
ok: Boolean(attempt.ok),
|
|
373
404
|
method: attempt.method || "",
|
|
374
405
|
reason: attempt.reason || null,
|
|
@@ -39,6 +39,7 @@ export async function refreshRecruitSearchAtEnd(client, {
|
|
|
39
39
|
forced_recent_viewed: Boolean(forceRecentViewed),
|
|
40
40
|
search_params: refreshSearchParams,
|
|
41
41
|
card_count: cardCount,
|
|
42
|
+
recovery_settle: application.reset?.mini_fresh_start || null,
|
|
42
43
|
application
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -129,6 +129,14 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
129
129
|
forced_recent_viewed: Boolean(refreshAttempt.forced_recent_viewed),
|
|
130
130
|
card_count: refreshAttempt.card_count || 0,
|
|
131
131
|
search_params: refreshAttempt.search_params || null,
|
|
132
|
+
recovery_settle: refreshAttempt.recovery_settle
|
|
133
|
+
? {
|
|
134
|
+
ok: Boolean(refreshAttempt.recovery_settle.ok),
|
|
135
|
+
status: refreshAttempt.recovery_settle.status || "",
|
|
136
|
+
reason: refreshAttempt.recovery_settle.reason || "",
|
|
137
|
+
elapsed_ms: refreshAttempt.recovery_settle.elapsed_ms || 0
|
|
138
|
+
}
|
|
139
|
+
: null,
|
|
132
140
|
application: refreshAttempt.application
|
|
133
141
|
? {
|
|
134
142
|
applied: Boolean(refreshAttempt.application.applied),
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
htmlToText,
|
|
18
18
|
normalizeText
|
|
19
19
|
} from "../../core/screening/index.js";
|
|
20
|
+
import {
|
|
21
|
+
createRecoverySettleError,
|
|
22
|
+
waitForMiniFreshStartSettle
|
|
23
|
+
} from "../common/recovery-settle.js";
|
|
20
24
|
import {
|
|
21
25
|
RECRUIT_CARD_SELECTOR,
|
|
22
26
|
RECRUIT_TARGET_URL,
|
|
@@ -402,12 +406,30 @@ export async function waitForRecruitSearchControls(client, {
|
|
|
402
406
|
};
|
|
403
407
|
}
|
|
404
408
|
|
|
409
|
+
async function settleRecruitSearchAfterReset(client, {
|
|
410
|
+
timeoutMs = DEFAULT_RECRUIT_RESET_TIMEOUT_MS,
|
|
411
|
+
settleMs = 5000
|
|
412
|
+
} = {}) {
|
|
413
|
+
return waitForMiniFreshStartSettle(client, {
|
|
414
|
+
domain: "search",
|
|
415
|
+
timeoutMs,
|
|
416
|
+
intervalMs: 500,
|
|
417
|
+
settleMs: Math.max(0, Math.min(settleMs || 0, 5000)),
|
|
418
|
+
readinessLabel: "search_controls_ready",
|
|
419
|
+
checkReady: ({ remainingMs }) => waitForRecruitSearchControls(client, {
|
|
420
|
+
timeoutMs: Math.min(Math.max(1, remainingMs), 1500),
|
|
421
|
+
intervalMs: 300
|
|
422
|
+
})
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
405
426
|
export async function resetRecruitSearchPage(client, {
|
|
406
427
|
url = RECRUIT_TARGET_URL,
|
|
407
428
|
settleMs = 5000,
|
|
408
429
|
timeoutMs = DEFAULT_RECRUIT_RESET_TIMEOUT_MS
|
|
409
430
|
} = {}) {
|
|
410
431
|
const actions = [];
|
|
432
|
+
let miniFreshStart = null;
|
|
411
433
|
const rootTimeoutMs = Math.min(timeoutMs, 90000);
|
|
412
434
|
async function waitForRootsAfterSettle() {
|
|
413
435
|
await sleep(settleMs);
|
|
@@ -432,6 +454,21 @@ export async function resetRecruitSearchPage(client, {
|
|
|
432
454
|
actions.push({ method: "Page.navigate", url });
|
|
433
455
|
}
|
|
434
456
|
|
|
457
|
+
miniFreshStart = await settleRecruitSearchAfterReset(client, {
|
|
458
|
+
timeoutMs: Math.min(timeoutMs, 90000),
|
|
459
|
+
settleMs
|
|
460
|
+
});
|
|
461
|
+
actions.push({
|
|
462
|
+
method: "mini_fresh_start_settle",
|
|
463
|
+
ok: Boolean(miniFreshStart.ok),
|
|
464
|
+
status: miniFreshStart.status || "",
|
|
465
|
+
reason: miniFreshStart.reason || "",
|
|
466
|
+
elapsed_ms: miniFreshStart.elapsed_ms || 0
|
|
467
|
+
});
|
|
468
|
+
if (!miniFreshStart.ok) {
|
|
469
|
+
throw createRecoverySettleError("search", miniFreshStart);
|
|
470
|
+
}
|
|
471
|
+
|
|
435
472
|
let roots = await waitForRootsAfterSettle();
|
|
436
473
|
const frameReset = await navigateRecruitSearchFrame(client, roots?.iframe?.nodeId, {
|
|
437
474
|
pageUrl: url,
|
|
@@ -459,6 +496,20 @@ export async function resetRecruitSearchPage(client, {
|
|
|
459
496
|
actions.push(fallbackFrameReset);
|
|
460
497
|
await sleep(settleMs);
|
|
461
498
|
}
|
|
499
|
+
miniFreshStart = await settleRecruitSearchAfterReset(client, {
|
|
500
|
+
timeoutMs: Math.min(timeoutMs, 90000),
|
|
501
|
+
settleMs: Math.min(settleMs, 1500)
|
|
502
|
+
});
|
|
503
|
+
actions.push({
|
|
504
|
+
method: "mini_fresh_start_settle_after_navigate",
|
|
505
|
+
ok: Boolean(miniFreshStart.ok),
|
|
506
|
+
status: miniFreshStart.status || "",
|
|
507
|
+
reason: miniFreshStart.reason || "",
|
|
508
|
+
elapsed_ms: miniFreshStart.elapsed_ms || 0
|
|
509
|
+
});
|
|
510
|
+
if (!miniFreshStart.ok) {
|
|
511
|
+
throw createRecoverySettleError("search", miniFreshStart);
|
|
512
|
+
}
|
|
462
513
|
controls = await waitForControls();
|
|
463
514
|
}
|
|
464
515
|
roots = await getRecruitRoots(client, { requireFrame: false });
|
|
@@ -473,6 +524,7 @@ export async function resetRecruitSearchPage(client, {
|
|
|
473
524
|
target_url: url,
|
|
474
525
|
iframe_selector: controls.iframe_selector || roots.iframe.selector,
|
|
475
526
|
iframe_document_node_id: controls.iframe_document_node_id || roots.iframe.documentNodeId,
|
|
527
|
+
mini_fresh_start: miniFreshStart,
|
|
476
528
|
controls
|
|
477
529
|
};
|
|
478
530
|
}
|