@reconcrap/boss-recommend-mcp 2.0.48 → 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/detail.js +1 -1
- package/src/domains/recommend/jobs.js +249 -23
- package/src/domains/recommend/refresh.js +89 -6
- package/src/domains/recommend/run-service.js +90 -38
- 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
|
+
}
|
|
@@ -312,7 +312,7 @@ export async function readRecommendDetailHtml(client, detailState) {
|
|
|
312
312
|
|
|
313
313
|
export function isStaleRecommendNodeError(error) {
|
|
314
314
|
const message = String(error?.message || error || "");
|
|
315
|
-
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
|
|
315
|
+
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i.test(message);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
export function isRecommendDetailOpenMissError(error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clickNodeCenter,
|
|
3
|
+
clickPoint,
|
|
3
4
|
DETERMINISTIC_CLICK_OPTIONS,
|
|
4
5
|
getAttributesMap,
|
|
5
6
|
getNodeBox,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
htmlToText,
|
|
13
14
|
normalizeText
|
|
14
15
|
} from "../../core/screening/index.js";
|
|
16
|
+
import { isStaleRecommendNodeError } from "./detail.js";
|
|
15
17
|
|
|
16
18
|
export const RECOMMEND_JOB_SELECTORS = Object.freeze({
|
|
17
19
|
trigger: ".job-selecter-wrap, [class*=\"job-selecter-wrap\"], .ui-dropmenu",
|
|
@@ -52,15 +54,26 @@ function isVisibleBox(box) {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
async function readJobOption(client, nodeId, index) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
let attributes = null;
|
|
58
|
+
let outerHTML = "";
|
|
59
|
+
try {
|
|
60
|
+
[attributes, outerHTML] = await Promise.all([
|
|
61
|
+
getAttributesMap(client, nodeId),
|
|
62
|
+
getOuterHTML(client, nodeId)
|
|
63
|
+
]);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isStaleRecommendNodeError(error)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
59
70
|
const label = normalizeText(htmlToText(outerHTML));
|
|
60
71
|
let box = null;
|
|
61
72
|
try {
|
|
62
73
|
box = await getNodeBox(client, nodeId);
|
|
63
|
-
} catch {
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!isStaleRecommendNodeError(error)) throw error;
|
|
76
|
+
}
|
|
64
77
|
const className = attributes.class || "";
|
|
65
78
|
return {
|
|
66
79
|
node_id: nodeId,
|
|
@@ -75,19 +88,40 @@ async function readJobOption(client, nodeId, index) {
|
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
async function readJobTrigger(client, nodeId) {
|
|
92
|
+
let box = null;
|
|
93
|
+
try {
|
|
94
|
+
box = await getNodeBox(client, nodeId);
|
|
95
|
+
} catch {}
|
|
96
|
+
if (!isVisibleBox(box)) return null;
|
|
97
|
+
|
|
98
|
+
let label = "";
|
|
99
|
+
let className = "";
|
|
100
|
+
try {
|
|
101
|
+
const outerHTML = await getOuterHTML(client, nodeId);
|
|
102
|
+
label = normalizeText(htmlToText(outerHTML));
|
|
103
|
+
} catch {}
|
|
104
|
+
try {
|
|
105
|
+
const attributes = await getAttributesMap(client, nodeId);
|
|
106
|
+
className = attributes.class || "";
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
node_id: nodeId,
|
|
111
|
+
center: box.center,
|
|
112
|
+
rect: box.rect,
|
|
113
|
+
label,
|
|
114
|
+
label_without_salary: trimSalarySuffix(label),
|
|
115
|
+
class_name: className,
|
|
116
|
+
visible: true
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
78
120
|
export async function findRecommendJobTrigger(client, frameNodeId) {
|
|
79
121
|
const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_JOB_SELECTORS.trigger);
|
|
80
122
|
for (const nodeId of nodeIds) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (isVisibleBox(box)) {
|
|
84
|
-
return {
|
|
85
|
-
node_id: nodeId,
|
|
86
|
-
center: box.center,
|
|
87
|
-
rect: box.rect
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
} catch {}
|
|
123
|
+
const trigger = await readJobTrigger(client, nodeId);
|
|
124
|
+
if (trigger) return trigger;
|
|
91
125
|
}
|
|
92
126
|
return null;
|
|
93
127
|
}
|
|
@@ -162,6 +196,7 @@ export async function openRecommendJobDropdown(client, frameNodeId, {
|
|
|
162
196
|
}
|
|
163
197
|
}
|
|
164
198
|
const error = new Error("Recommend job dropdown did not expose visible options after trigger click");
|
|
199
|
+
error.trigger = trigger;
|
|
165
200
|
error.job_dropdown_attempts = attempts;
|
|
166
201
|
throw error;
|
|
167
202
|
}
|
|
@@ -204,6 +239,7 @@ export async function listRecommendJobOptions(client, frameNodeId, {
|
|
|
204
239
|
if (seen.has(nodeId)) continue;
|
|
205
240
|
seen.add(nodeId);
|
|
206
241
|
const option = await readJobOption(client, nodeId, index);
|
|
242
|
+
if (!option) continue;
|
|
207
243
|
if (!option.label) continue;
|
|
208
244
|
if (option.label.length > 120) continue;
|
|
209
245
|
options.push(option);
|
|
@@ -230,6 +266,150 @@ export async function closeRecommendJobDropdown(client) {
|
|
|
230
266
|
};
|
|
231
267
|
}
|
|
232
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
|
+
|
|
233
413
|
export async function selectRecommendJob(client, frameNodeId, {
|
|
234
414
|
jobLabel = "",
|
|
235
415
|
settleMs = 6000,
|
|
@@ -245,10 +425,42 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
245
425
|
};
|
|
246
426
|
}
|
|
247
427
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
428
|
+
let opened = null;
|
|
429
|
+
try {
|
|
430
|
+
opened = await openRecommendJobDropdown(client, frameNodeId, {
|
|
431
|
+
timeoutMs: dropdownTimeoutMs,
|
|
432
|
+
triggerTimeoutMs: dropdownTimeoutMs
|
|
433
|
+
});
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const currentOptions = await listRecommendJobOptions(client, frameNodeId, {
|
|
436
|
+
openDropdown: false
|
|
437
|
+
}).catch(() => []);
|
|
438
|
+
const currentMatch = currentOptions.find((option) => (
|
|
439
|
+
option.current && jobLabelMatches(option.label, target)
|
|
440
|
+
));
|
|
441
|
+
if (currentMatch) {
|
|
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
|
+
}));
|
|
448
|
+
return {
|
|
449
|
+
requested: target,
|
|
450
|
+
selected: true,
|
|
451
|
+
already_current: true,
|
|
452
|
+
selected_option: compactJobOption({
|
|
453
|
+
...currentMatch,
|
|
454
|
+
source: "current_option_without_visible_dropdown"
|
|
455
|
+
}),
|
|
456
|
+
options: currentOptions.map(compactJobOption),
|
|
457
|
+
dropdown_error: error?.message || String(error),
|
|
458
|
+
job_dropdown_attempts: error?.job_dropdown_attempts || [],
|
|
459
|
+
menu_close: menuClose
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
252
464
|
const options = opened.options.length
|
|
253
465
|
? opened.options
|
|
254
466
|
: await listRecommendJobOptions(client, frameNodeId, { openDropdown: false });
|
|
@@ -272,13 +484,19 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
272
484
|
}
|
|
273
485
|
|
|
274
486
|
if (match.current) {
|
|
275
|
-
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
|
+
}));
|
|
276
493
|
return {
|
|
277
494
|
requested: target,
|
|
278
495
|
selected: true,
|
|
279
496
|
already_current: true,
|
|
280
497
|
selected_option: compactJobOption(match),
|
|
281
|
-
options: options.map(compactJobOption)
|
|
498
|
+
options: options.map(compactJobOption),
|
|
499
|
+
menu_close: menuClose
|
|
282
500
|
};
|
|
283
501
|
}
|
|
284
502
|
|
|
@@ -289,6 +507,12 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
289
507
|
|
|
290
508
|
const clickedBox = await clickNodeCenter(client, match.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
291
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
|
+
}));
|
|
292
516
|
return {
|
|
293
517
|
requested: target,
|
|
294
518
|
selected: true,
|
|
@@ -298,7 +522,8 @@ export async function selectRecommendJob(client, frameNodeId, {
|
|
|
298
522
|
center: clickedBox.center,
|
|
299
523
|
rect: clickedBox.rect
|
|
300
524
|
},
|
|
301
|
-
options: options.map(compactJobOption)
|
|
525
|
+
options: options.map(compactJobOption),
|
|
526
|
+
menu_close: menuClose
|
|
302
527
|
};
|
|
303
528
|
}
|
|
304
529
|
|
|
@@ -311,6 +536,7 @@ function compactJobOption(option) {
|
|
|
311
536
|
class_name: option.class_name,
|
|
312
537
|
node_id: option.node_id,
|
|
313
538
|
center: option.center,
|
|
314
|
-
rect: option.rect
|
|
539
|
+
rect: option.rect,
|
|
540
|
+
source: option.source || null
|
|
315
541
|
};
|
|
316
542
|
}
|
|
@@ -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,12 +16,16 @@ 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,
|
|
15
26
|
waitForRecommendRoots
|
|
16
27
|
} from "./roots.js";
|
|
28
|
+
import { isStaleRecommendNodeError } from "./detail.js";
|
|
17
29
|
|
|
18
30
|
function normalizeLabels(labels = []) {
|
|
19
31
|
return labels.map((label) => String(label || "").trim()).filter(Boolean);
|
|
@@ -102,8 +114,9 @@ function compactFilterReapplyError(error) {
|
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
export function isRetryableRecommendJobSelectionError(error) {
|
|
117
|
+
if (isStaleRecommendNodeError(error)) return true;
|
|
105
118
|
const message = String(error?.message || error || "");
|
|
106
|
-
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);
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
function compactJobSelectionAttempt({
|
|
@@ -121,10 +134,40 @@ function compactJobSelectionAttempt({
|
|
|
121
134
|
attempt,
|
|
122
135
|
iframe_document_node_id: iframeDocumentNodeId || 0,
|
|
123
136
|
selected: Boolean(selection?.selected),
|
|
124
|
-
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
|
|
125
143
|
};
|
|
126
144
|
}
|
|
127
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
|
+
|
|
160
|
+
async function waitForFreshRecommendRoots(client, {
|
|
161
|
+
timeoutMs = 10000,
|
|
162
|
+
intervalMs = 500
|
|
163
|
+
} = {}) {
|
|
164
|
+
const rootState = await waitForRecommendRoots(client, {
|
|
165
|
+
timeoutMs,
|
|
166
|
+
intervalMs
|
|
167
|
+
});
|
|
168
|
+
return rootState?.iframe?.documentNodeId ? rootState : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
128
171
|
export async function selectRecommendJobWithRootRefresh(client, rootState, {
|
|
129
172
|
jobLabel = "",
|
|
130
173
|
settleMs = 6000,
|
|
@@ -141,7 +184,10 @@ export async function selectRecommendJobWithRootRefresh(client, rootState, {
|
|
|
141
184
|
while (Date.now() - started <= totalTimeoutMs) {
|
|
142
185
|
attempt += 1;
|
|
143
186
|
if (!currentRootState?.iframe?.documentNodeId) {
|
|
144
|
-
currentRootState = await
|
|
187
|
+
currentRootState = await waitForFreshRecommendRoots(client, {
|
|
188
|
+
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
189
|
+
intervalMs: 500
|
|
190
|
+
});
|
|
145
191
|
}
|
|
146
192
|
const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
|
|
147
193
|
try {
|
|
@@ -150,6 +196,31 @@ export async function selectRecommendJobWithRootRefresh(client, rootState, {
|
|
|
150
196
|
settleMs,
|
|
151
197
|
dropdownTimeoutMs
|
|
152
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
|
+
}
|
|
153
224
|
attempts.push(compactJobSelectionAttempt({
|
|
154
225
|
ok: true,
|
|
155
226
|
attempt,
|
|
@@ -176,7 +247,10 @@ export async function selectRecommendJobWithRootRefresh(client, rootState, {
|
|
|
176
247
|
break;
|
|
177
248
|
}
|
|
178
249
|
if (retryDelayMs > 0) await sleep(retryDelayMs);
|
|
179
|
-
currentRootState = await
|
|
250
|
+
currentRootState = await waitForFreshRecommendRoots(client, {
|
|
251
|
+
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
252
|
+
intervalMs: 500
|
|
253
|
+
});
|
|
180
254
|
}
|
|
181
255
|
}
|
|
182
256
|
|
|
@@ -251,13 +325,20 @@ async function applyRefreshMethod(client, method, {
|
|
|
251
325
|
let pageScopeResult = null;
|
|
252
326
|
let filterResult = null;
|
|
253
327
|
let filterReapplyAttempts = [];
|
|
328
|
+
let recoverySettle = null;
|
|
254
329
|
try {
|
|
255
330
|
if (method === "page_navigate") {
|
|
256
331
|
await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
|
|
257
332
|
} else {
|
|
258
333
|
await client.Page.reload({ ignoreCache: true });
|
|
259
334
|
}
|
|
260
|
-
|
|
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
|
+
}
|
|
261
342
|
currentRootState = await waitForRecommendRoots(client, {
|
|
262
343
|
timeoutMs: Math.max(45000, reloadSettleMs * 6),
|
|
263
344
|
intervalMs: 500
|
|
@@ -318,6 +399,7 @@ async function applyRefreshMethod(client, method, {
|
|
|
318
399
|
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
319
400
|
job_selection: jobSelection,
|
|
320
401
|
job_selection_attempts: jobSelectionAttempts,
|
|
402
|
+
recovery_settle: recoverySettle,
|
|
321
403
|
page_scope: pageScopeResult,
|
|
322
404
|
filter: filterResult,
|
|
323
405
|
filter_reapply_attempts: filterReapplyAttempts,
|
|
@@ -335,6 +417,7 @@ async function applyRefreshMethod(client, method, {
|
|
|
335
417
|
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
336
418
|
job_selection: jobSelection,
|
|
337
419
|
job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
|
|
420
|
+
recovery_settle: error?.recovery_settle || recoverySettle,
|
|
338
421
|
page_scope: pageScopeResult,
|
|
339
422
|
filter: filterResult,
|
|
340
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,
|
|
@@ -1256,11 +1287,13 @@ export async function runRecommendWorkflow({
|
|
|
1256
1287
|
: useLlmScreening
|
|
1257
1288
|
? llmResultToScreening(llmResult, screeningCandidate)
|
|
1258
1289
|
: screenCandidate(screeningCandidate, { criteria });
|
|
1259
|
-
let actionDiscovery = null;
|
|
1260
|
-
let postActionResult = null;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1290
|
+
let actionDiscovery = null;
|
|
1291
|
+
let postActionResult = null;
|
|
1292
|
+
let closeFailureError = null;
|
|
1293
|
+
let closeRecoveryFailure = null;
|
|
1294
|
+
if (postActionEnabled && detailResult) {
|
|
1295
|
+
const postActionStarted = Date.now();
|
|
1296
|
+
await runControl.waitIfPaused();
|
|
1264
1297
|
runControl.throwIfCanceled();
|
|
1265
1298
|
runControl.setPhase("recommend:post-action");
|
|
1266
1299
|
await maybeHumanActionCooldown("before_post_action", timings);
|
|
@@ -1288,21 +1321,34 @@ export async function runRecommendWorkflow({
|
|
|
1288
1321
|
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
1289
1322
|
await maybeHumanActionCooldown("after_detail_close", timings);
|
|
1290
1323
|
if (!detailResult.close_result?.closed) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1324
|
+
closeFailureError = createRecommendCloseFailureError(detailResult.close_result);
|
|
1325
|
+
try {
|
|
1326
|
+
const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeFailureError, {
|
|
1327
|
+
forceRecentNotView: true
|
|
1328
|
+
});
|
|
1329
|
+
detailResult.cv_acquisition = {
|
|
1330
|
+
...(detailResult.cv_acquisition || {}),
|
|
1331
|
+
close_recovery: {
|
|
1332
|
+
ok: Boolean(recovery.ok),
|
|
1333
|
+
method: recovery.method || "",
|
|
1334
|
+
forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
|
|
1335
|
+
card_count: recovery.card_count || 0
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
closeRecoveryFailure = error;
|
|
1340
|
+
detailResult.cv_acquisition = {
|
|
1341
|
+
...(detailResult.cv_acquisition || {}),
|
|
1342
|
+
close_recovery: {
|
|
1343
|
+
ok: false,
|
|
1344
|
+
reason: "context_recovery_failed",
|
|
1345
|
+
error: error?.message || String(error),
|
|
1346
|
+
forced_recent_not_view: true
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1306
1352
|
timings.total_ms = Date.now() - candidateStarted;
|
|
1307
1353
|
const compactResult = {
|
|
1308
1354
|
index,
|
|
@@ -1313,12 +1359,14 @@ export async function runRecommendWorkflow({
|
|
|
1313
1359
|
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
1314
1360
|
screening: compactScreening(screening),
|
|
1315
1361
|
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
1316
|
-
post_action: postActionResult,
|
|
1317
|
-
error: recoverableDetailError
|
|
1318
|
-
? compactRecoverableDetailError(recoverableDetailError)
|
|
1319
|
-
:
|
|
1320
|
-
? compactError(
|
|
1321
|
-
|
|
1362
|
+
post_action: postActionResult,
|
|
1363
|
+
error: recoverableDetailError
|
|
1364
|
+
? compactRecoverableDetailError(recoverableDetailError)
|
|
1365
|
+
: closeRecoveryFailure
|
|
1366
|
+
? compactError(closeFailureError, "DETAIL_CLOSE_FAILED")
|
|
1367
|
+
: detailResult?.image_evidence?.ok === false
|
|
1368
|
+
? compactError({
|
|
1369
|
+
code: detailResult.image_evidence.error_code,
|
|
1322
1370
|
message: detailResult.image_evidence.error
|
|
1323
1371
|
}, "IMAGE_CAPTURE_FAILED")
|
|
1324
1372
|
: null,
|
|
@@ -1353,9 +1401,13 @@ export async function runRecommendWorkflow({
|
|
|
1353
1401
|
error: compactResult.error,
|
|
1354
1402
|
post_action: postActionResult
|
|
1355
1403
|
}
|
|
1356
|
-
});
|
|
1357
|
-
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1358
|
-
|
|
1404
|
+
});
|
|
1405
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1406
|
+
|
|
1407
|
+
if (closeRecoveryFailure) {
|
|
1408
|
+
throw closeRecoveryFailure;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1359
1411
|
if (postActionResult?.stop_run) {
|
|
1360
1412
|
listEndReason = postActionResult.reason || "post_action_stop";
|
|
1361
1413
|
break;
|
|
@@ -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
|
}
|