@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23
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 +2 -1
- 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 +2416 -2385
- 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 +1315 -1306
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +44 -27
- 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 +571 -571
- 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 +655 -646
- 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 +575 -351
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1616 -878
- 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 +121 -4
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +1174 -914
- package/src/recommend-scheduler.js +535 -469
- package/src/recruit-mcp.js +2121 -2121
|
@@ -1,266 +1,438 @@
|
|
|
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";
|
|
10
|
-
import {
|
|
11
|
-
clickRecommendEndRefreshButton,
|
|
12
|
-
waitForRecommendCardNodeIds
|
|
13
|
-
} from "./cards.js";
|
|
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";
|
|
10
|
+
import {
|
|
11
|
+
clickRecommendEndRefreshButton,
|
|
12
|
+
waitForRecommendCardNodeIds
|
|
13
|
+
} from "./cards.js";
|
|
14
14
|
import {
|
|
15
15
|
RECOMMEND_RECENT_NOT_VIEW_LABEL,
|
|
16
16
|
RECOMMEND_TARGET_URL
|
|
17
17
|
} from "./constants.js";
|
|
18
18
|
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
19
|
+
import { ensureRecommendCurrentCityOnly } from "./location.js";
|
|
19
20
|
import {
|
|
20
21
|
selectRecommendJob,
|
|
21
22
|
verifyRecommendJobSelection
|
|
22
23
|
} from "./jobs.js";
|
|
23
24
|
import { selectRecommendPageScope } from "./scopes.js";
|
|
24
|
-
import {
|
|
25
|
-
getRecommendRoots,
|
|
26
|
-
waitForRecommendRoots
|
|
27
|
-
} from "./roots.js";
|
|
28
|
-
import { isStaleRecommendNodeError } from "./detail.js";
|
|
29
|
-
|
|
30
|
-
function normalizeLabels(labels = []) {
|
|
31
|
-
return labels.map((label) => String(label || "").trim()).filter(Boolean);
|
|
32
|
-
}
|
|
25
|
+
import {
|
|
26
|
+
getRecommendRoots,
|
|
27
|
+
waitForRecommendRoots
|
|
28
|
+
} from "./roots.js";
|
|
29
|
+
import { isStaleRecommendNodeError } from "./detail.js";
|
|
30
|
+
|
|
31
|
+
function normalizeLabels(labels = []) {
|
|
32
|
+
return labels.map((label) => String(label || "").trim()).filter(Boolean);
|
|
33
|
+
}
|
|
33
34
|
|
|
34
35
|
function normalizeFilterGroup(spec = {}) {
|
|
35
36
|
return {
|
|
36
37
|
group: String(spec.group || "").trim(),
|
|
37
38
|
labels: normalizeLabels(spec.labels || spec.filterLabels || []),
|
|
38
|
-
selectAllLabels: spec.selectAllLabels !== false
|
|
39
|
+
selectAllLabels: spec.selectAllLabels !== false,
|
|
40
|
+
allowUnlimited: spec.allowUnlimited === true,
|
|
41
|
+
verifySticky: spec.verifySticky === true
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export function buildRecommendFilterGroups(filter = {}, {
|
|
43
46
|
forceRecentNotView = false
|
|
44
|
-
} = {}) {
|
|
45
|
-
const groups = [];
|
|
46
|
-
const sourceGroups = Array.isArray(filter.filterGroups)
|
|
47
|
-
? filter.filterGroups
|
|
48
|
-
: Array.isArray(filter.groups)
|
|
49
|
-
? filter.groups
|
|
50
|
-
: [];
|
|
51
|
-
|
|
52
|
-
for (const spec of sourceGroups) {
|
|
53
|
-
const group = normalizeFilterGroup(spec);
|
|
54
|
-
if (group.group || group.labels.length) groups.push(group);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const rootGroup = normalizeFilterGroup(filter);
|
|
58
|
-
if ((rootGroup.group || rootGroup.labels.length) && !groups.length) {
|
|
59
|
-
groups.push(rootGroup);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (forceRecentNotView) {
|
|
63
|
-
const recentGroup = groups.find((item) => item.group === "recentNotView");
|
|
64
|
-
if (recentGroup) {
|
|
65
|
-
if (!recentGroup.labels.some((label) => label.replace(/\s+/g, "") === RECOMMEND_RECENT_NOT_VIEW_LABEL)) {
|
|
66
|
-
recentGroup.labels.push(RECOMMEND_RECENT_NOT_VIEW_LABEL);
|
|
67
|
-
}
|
|
47
|
+
} = {}) {
|
|
48
|
+
const groups = [];
|
|
49
|
+
const sourceGroups = Array.isArray(filter.filterGroups)
|
|
50
|
+
? filter.filterGroups
|
|
51
|
+
: Array.isArray(filter.groups)
|
|
52
|
+
? filter.groups
|
|
53
|
+
: [];
|
|
54
|
+
|
|
55
|
+
for (const spec of sourceGroups) {
|
|
56
|
+
const group = normalizeFilterGroup(spec);
|
|
57
|
+
if (group.group || group.labels.length) groups.push(group);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rootGroup = normalizeFilterGroup(filter);
|
|
61
|
+
if ((rootGroup.group || rootGroup.labels.length) && !groups.length) {
|
|
62
|
+
groups.push(rootGroup);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (forceRecentNotView) {
|
|
66
|
+
const recentGroup = groups.find((item) => item.group === "recentNotView");
|
|
67
|
+
if (recentGroup) {
|
|
68
|
+
if (!recentGroup.labels.some((label) => label.replace(/\s+/g, "") === RECOMMEND_RECENT_NOT_VIEW_LABEL)) {
|
|
69
|
+
recentGroup.labels.push(RECOMMEND_RECENT_NOT_VIEW_LABEL);
|
|
70
|
+
}
|
|
68
71
|
recentGroup.selectAllLabels = true;
|
|
69
72
|
} else {
|
|
70
73
|
groups.unshift({
|
|
71
74
|
group: "recentNotView",
|
|
72
75
|
labels: [RECOMMEND_RECENT_NOT_VIEW_LABEL],
|
|
73
|
-
selectAllLabels: true
|
|
76
|
+
selectAllLabels: true,
|
|
77
|
+
allowUnlimited: false,
|
|
78
|
+
verifySticky: false
|
|
74
79
|
});
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
return groups;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function buildRecommendFilterSelectionOptions(filter = {}, {
|
|
82
|
-
forceRecentNotView = false
|
|
83
|
-
} = {}) {
|
|
84
|
-
const filterGroups = buildRecommendFilterGroups(filter, { forceRecentNotView });
|
|
85
|
-
if (filterGroups.length > 1 || forceRecentNotView || Array.isArray(filter.filterGroups) || Array.isArray(filter.groups)) {
|
|
86
|
-
return { filterGroups };
|
|
87
|
-
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildRecommendFilterSelectionOptions(filter = {}, {
|
|
87
|
+
forceRecentNotView = false
|
|
88
|
+
} = {}) {
|
|
89
|
+
const filterGroups = buildRecommendFilterGroups(filter, { forceRecentNotView });
|
|
90
|
+
if (filterGroups.length > 1 || forceRecentNotView || Array.isArray(filter.filterGroups) || Array.isArray(filter.groups)) {
|
|
91
|
+
return { filterGroups };
|
|
92
|
+
}
|
|
88
93
|
const [singleGroup] = filterGroups;
|
|
89
94
|
if (singleGroup) {
|
|
90
95
|
return {
|
|
91
96
|
group: singleGroup.group,
|
|
92
97
|
labels: singleGroup.labels,
|
|
93
|
-
selectAllLabels: singleGroup.selectAllLabels
|
|
98
|
+
selectAllLabels: singleGroup.selectAllLabels,
|
|
99
|
+
allowUnlimited: singleGroup.allowUnlimited,
|
|
100
|
+
verifySticky: singleGroup.verifySticky
|
|
94
101
|
};
|
|
95
102
|
}
|
|
96
103
|
return {
|
|
97
104
|
group: filter.group || "",
|
|
98
105
|
labels: normalizeLabels(filter.labels || filter.filterLabels || []),
|
|
99
|
-
selectAllLabels: filter.selectAllLabels !== false
|
|
106
|
+
selectAllLabels: filter.selectAllLabels !== false,
|
|
107
|
+
allowUnlimited: filter.allowUnlimited === true,
|
|
108
|
+
verifySticky: filter.verifySticky === true
|
|
100
109
|
};
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
function
|
|
104
|
-
|
|
112
|
+
export async function applyRecommendFilterEnvelopeStages(filter = {}, {
|
|
113
|
+
applyCurrentCityOnly,
|
|
114
|
+
applyFilterPanel
|
|
115
|
+
} = {}) {
|
|
116
|
+
if (filter.enabled === false) {
|
|
117
|
+
return {
|
|
118
|
+
applied: false,
|
|
119
|
+
skipped: true,
|
|
120
|
+
current_city_only: null,
|
|
121
|
+
filter: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (typeof applyCurrentCityOnly !== "function" || typeof applyFilterPanel !== "function") {
|
|
125
|
+
throw new Error("Recommend native filter stages require location and filter-panel callbacks");
|
|
126
|
+
}
|
|
127
|
+
const currentCityOnlyResult = await applyCurrentCityOnly();
|
|
128
|
+
const filterResult = await applyFilterPanel();
|
|
129
|
+
return {
|
|
130
|
+
applied: true,
|
|
131
|
+
skipped: false,
|
|
132
|
+
current_city_only: currentCityOnlyResult,
|
|
133
|
+
filter: filterResult
|
|
134
|
+
};
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return /Recommend filter panel did not open|Recommend filter trigger was not found|Recommend filter confirm button was not found|No matching recommend filter option/i.test(message);
|
|
137
|
+
function refreshFailureReason(method = "") {
|
|
138
|
+
return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
|
|
110
139
|
}
|
|
111
140
|
|
|
112
|
-
function
|
|
113
|
-
|
|
141
|
+
function safeDiagnosticText(value, maxLength = 4000) {
|
|
142
|
+
const text = String(value ?? "");
|
|
143
|
+
return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
|
|
114
144
|
}
|
|
115
145
|
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
function safeDiagnosticStack(stack) {
|
|
147
|
+
if (!stack) return null;
|
|
148
|
+
return safeDiagnosticText(stack, 8000)
|
|
149
|
+
.split(/\r?\n/)
|
|
150
|
+
.slice(0, 12)
|
|
151
|
+
.join("\n");
|
|
120
152
|
}
|
|
121
153
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
iframeDocumentNodeId = 0,
|
|
126
|
-
error = null,
|
|
127
|
-
selection = null
|
|
154
|
+
function buildErrorDiagnostic(error, {
|
|
155
|
+
depth = 0,
|
|
156
|
+
seen = new Set()
|
|
128
157
|
} = {}) {
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
sticky_menu_closed: selection?.sticky_verification?.menu_close?.ok ?? null
|
|
158
|
+
if (!error) return null;
|
|
159
|
+
const isRecord = typeof error === "object" || typeof error === "function";
|
|
160
|
+
if (isRecord && seen.has(error)) {
|
|
161
|
+
return {
|
|
162
|
+
name: error?.name || "Error",
|
|
163
|
+
message: safeDiagnosticText(error?.message || String(error)),
|
|
164
|
+
circular: true
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (isRecord) seen.add(error);
|
|
168
|
+
const diagnostic = {
|
|
169
|
+
name: error?.name || "Error",
|
|
170
|
+
message: safeDiagnosticText(error?.message || String(error))
|
|
143
171
|
};
|
|
172
|
+
if (error?.code !== undefined && error?.code !== null && error.code !== "") {
|
|
173
|
+
diagnostic.code = typeof error.code === "number"
|
|
174
|
+
? error.code
|
|
175
|
+
: safeDiagnosticText(error.code, 300);
|
|
176
|
+
}
|
|
177
|
+
if (error?.phase) diagnostic.phase = safeDiagnosticText(error.phase, 300);
|
|
178
|
+
if (error?.cdp_method) diagnostic.cdp_method = safeDiagnosticText(error.cdp_method, 300);
|
|
179
|
+
if (error?.cdp_at) diagnostic.cdp_at = safeDiagnosticText(error.cdp_at, 100);
|
|
180
|
+
if (Number.isInteger(error?.cdp_node_id)) diagnostic.cdp_node_id = error.cdp_node_id;
|
|
181
|
+
if (Number.isInteger(error?.cdp_backend_node_id)) {
|
|
182
|
+
diagnostic.cdp_backend_node_id = error.cdp_backend_node_id;
|
|
183
|
+
}
|
|
184
|
+
if (Number.isInteger(error?.node_id)) diagnostic.node_id = error.node_id;
|
|
185
|
+
if (Number.isInteger(error?.backend_node_id)) diagnostic.backend_node_id = error.backend_node_id;
|
|
186
|
+
if (error?.cdp_search_id) diagnostic.cdp_search_id = safeDiagnosticText(error.cdp_search_id, 300);
|
|
187
|
+
if (Array.isArray(error?.cdp_param_keys)) {
|
|
188
|
+
diagnostic.cdp_param_keys = error.cdp_param_keys
|
|
189
|
+
.slice(0, 20)
|
|
190
|
+
.map((key) => safeDiagnosticText(key, 100));
|
|
191
|
+
}
|
|
192
|
+
const stack = safeDiagnosticStack(error?.stack);
|
|
193
|
+
if (stack) diagnostic.stack = stack;
|
|
194
|
+
if (depth < 2 && error?.cause && error.cause !== error) {
|
|
195
|
+
diagnostic.cause = buildErrorDiagnostic(error.cause, {
|
|
196
|
+
depth: depth + 1,
|
|
197
|
+
seen
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return diagnostic;
|
|
144
201
|
}
|
|
145
202
|
|
|
146
|
-
|
|
147
|
-
|
|
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;
|
|
203
|
+
export function compactRecommendRefreshErrorDiagnostic(error) {
|
|
204
|
+
return buildErrorDiagnostic(error);
|
|
169
205
|
}
|
|
170
206
|
|
|
171
|
-
export
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
let lastError = null;
|
|
182
|
-
let attempt = 0;
|
|
183
|
-
|
|
184
|
-
while (Date.now() - started <= totalTimeoutMs) {
|
|
185
|
-
attempt += 1;
|
|
186
|
-
if (!currentRootState?.iframe?.documentNodeId) {
|
|
187
|
-
currentRootState = await waitForFreshRecommendRoots(client, {
|
|
188
|
-
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
189
|
-
intervalMs: 500
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
|
|
193
|
-
try {
|
|
194
|
-
const selection = await selectRecommendJob(client, iframeDocumentNodeId, {
|
|
195
|
-
jobLabel,
|
|
196
|
-
settleMs,
|
|
197
|
-
dropdownTimeoutMs
|
|
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
|
-
}
|
|
224
|
-
attempts.push(compactJobSelectionAttempt({
|
|
225
|
-
ok: true,
|
|
226
|
-
attempt,
|
|
227
|
-
iframeDocumentNodeId,
|
|
228
|
-
selection
|
|
229
|
-
}));
|
|
230
|
-
return {
|
|
231
|
-
job_selection: {
|
|
232
|
-
...selection,
|
|
233
|
-
refresh_attempts: attempts
|
|
234
|
-
},
|
|
235
|
-
root_state: currentRootState,
|
|
236
|
-
attempts
|
|
237
|
-
};
|
|
238
|
-
} catch (error) {
|
|
239
|
-
lastError = error;
|
|
240
|
-
attempts.push(compactJobSelectionAttempt({
|
|
241
|
-
ok: false,
|
|
242
|
-
attempt,
|
|
243
|
-
iframeDocumentNodeId,
|
|
244
|
-
error
|
|
245
|
-
}));
|
|
246
|
-
if (!isRetryableRecommendJobSelectionError(error) || Date.now() - started >= totalTimeoutMs) {
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
if (retryDelayMs > 0) await sleep(retryDelayMs);
|
|
250
|
-
currentRootState = await waitForFreshRecommendRoots(client, {
|
|
251
|
-
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
252
|
-
intervalMs: 500
|
|
253
|
-
});
|
|
254
|
-
}
|
|
207
|
+
export function isRetryableRecommendFilterReapplyError(error) {
|
|
208
|
+
const messages = [];
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
let current = error;
|
|
211
|
+
for (let depth = 0; current && depth < 4; depth += 1) {
|
|
212
|
+
if ((typeof current === "object" || typeof current === "function") && seen.has(current)) break;
|
|
213
|
+
if (typeof current === "object" || typeof current === "function") seen.add(current);
|
|
214
|
+
if (isStaleRecommendNodeError(current)) return true;
|
|
215
|
+
messages.push(String(current?.message || current || ""));
|
|
216
|
+
current = current?.cause;
|
|
255
217
|
}
|
|
256
|
-
|
|
257
|
-
|
|
218
|
+
const message = messages.join("\n");
|
|
219
|
+
return /Recommend filter panel did not open|Recommend filter trigger was not found|Recommend filter confirm button was not found|No matching recommend filter option|Invalid (?:backend )?node(?:\s*id)?|Node with given id does not exist|No node found for given backend id/i.test(message);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function compactFilterReapplyError(error) {
|
|
223
|
+
return error?.message || String(error || "Recommend filter reapply failed");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isRetryableRecommendJobSelectionError(error) {
|
|
227
|
+
if (isStaleRecommendNodeError(error)) return true;
|
|
228
|
+
const message = String(error?.message || error || "");
|
|
229
|
+
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);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function compactJobSelectionAttempt({
|
|
233
|
+
ok = false,
|
|
234
|
+
attempt = 0,
|
|
235
|
+
iframeDocumentNodeId = 0,
|
|
236
|
+
error = null,
|
|
237
|
+
selection = null
|
|
238
|
+
} = {}) {
|
|
239
|
+
return {
|
|
240
|
+
ok: Boolean(ok),
|
|
241
|
+
method: "job_select",
|
|
242
|
+
reason: error ? "job_select_failed" : null,
|
|
243
|
+
error: error ? (error?.message || String(error)) : null,
|
|
244
|
+
error_diagnostic: error ? compactRecommendRefreshErrorDiagnostic(error) : null,
|
|
245
|
+
attempt,
|
|
246
|
+
iframe_document_node_id: iframeDocumentNodeId || 0,
|
|
247
|
+
selected: Boolean(selection?.selected),
|
|
248
|
+
selection_reason: selection?.reason || null,
|
|
249
|
+
sticky_verified: selection?.sticky_verification?.verified ?? null,
|
|
250
|
+
sticky_current_label: selection?.sticky_verification?.current_label_without_salary
|
|
251
|
+
|| selection?.sticky_verification?.current_label
|
|
252
|
+
|| null,
|
|
253
|
+
sticky_menu_closed: selection?.sticky_verification?.menu_close?.ok ?? null
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function waitForRecommendRecoverySettle(client, {
|
|
258
|
+
reloadSettleMs = 8000,
|
|
259
|
+
timeoutMs = 90000
|
|
260
|
+
} = {}) {
|
|
261
|
+
return waitForMiniFreshStartSettle(client, {
|
|
262
|
+
domain: "recommend",
|
|
263
|
+
timeoutMs,
|
|
264
|
+
intervalMs: reloadSettleMs > 10000 ? 1200 : 800,
|
|
265
|
+
settleMs: Math.max(0, Math.min(reloadSettleMs || 0, 5000)),
|
|
266
|
+
selfHealConfig: buildRecommendSelfHealConfig(),
|
|
267
|
+
resolveSelfHealRoots: resolveRecommendSelfHealRoots
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function waitForFreshRecommendRoots(client, {
|
|
272
|
+
timeoutMs = 10000,
|
|
273
|
+
intervalMs = 500
|
|
274
|
+
} = {}) {
|
|
275
|
+
const rootState = await waitForRecommendRoots(client, {
|
|
276
|
+
timeoutMs,
|
|
277
|
+
intervalMs
|
|
278
|
+
});
|
|
279
|
+
return rootState?.iframe?.documentNodeId ? rootState : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function selectRecommendJobWithRootRefresh(client, rootState, {
|
|
283
|
+
jobLabel = "",
|
|
284
|
+
settleMs = 6000,
|
|
285
|
+
dropdownTimeoutMs = 4000,
|
|
286
|
+
totalTimeoutMs = 30000,
|
|
287
|
+
retryDelayMs = 1000
|
|
288
|
+
} = {}) {
|
|
289
|
+
const started = Date.now();
|
|
290
|
+
const attempts = [];
|
|
291
|
+
let currentRootState = rootState || null;
|
|
292
|
+
let lastError = null;
|
|
293
|
+
let attempt = 0;
|
|
294
|
+
|
|
295
|
+
while (Date.now() - started <= totalTimeoutMs) {
|
|
296
|
+
attempt += 1;
|
|
297
|
+
if (!currentRootState?.iframe?.documentNodeId) {
|
|
298
|
+
currentRootState = await waitForFreshRecommendRoots(client, {
|
|
299
|
+
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
300
|
+
intervalMs: 500
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const iframeDocumentNodeId = currentRootState?.iframe?.documentNodeId || 0;
|
|
304
|
+
try {
|
|
305
|
+
const selection = await selectRecommendJob(client, iframeDocumentNodeId, {
|
|
306
|
+
jobLabel,
|
|
307
|
+
settleMs,
|
|
308
|
+
dropdownTimeoutMs
|
|
309
|
+
});
|
|
310
|
+
if (selection.selected) {
|
|
311
|
+
const stickyRootState = await waitForFreshRecommendRoots(client, {
|
|
312
|
+
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
313
|
+
intervalMs: 500
|
|
314
|
+
}) || currentRootState;
|
|
315
|
+
const stickyFrameNodeId = stickyRootState?.iframe?.documentNodeId || iframeDocumentNodeId;
|
|
316
|
+
const stickyVerification = await verifyRecommendJobSelection(client, stickyFrameNodeId, {
|
|
317
|
+
jobLabel,
|
|
318
|
+
delayMs: 2000,
|
|
319
|
+
dropdownTimeoutMs,
|
|
320
|
+
closeSettleMs: 300
|
|
321
|
+
});
|
|
322
|
+
selection.sticky_verification = stickyVerification;
|
|
323
|
+
currentRootState = stickyRootState || currentRootState;
|
|
324
|
+
if (!stickyVerification.verified) {
|
|
325
|
+
const stickyError = new Error(`Recommend job selection was not sticky after 2s: requested=${jobLabel}; current=${stickyVerification.current_label_without_salary || stickyVerification.current_label || "unknown"}`);
|
|
326
|
+
stickyError.sticky_verification = stickyVerification;
|
|
327
|
+
throw stickyError;
|
|
328
|
+
}
|
|
329
|
+
if (stickyVerification.menu_close && stickyVerification.menu_close.ok === false) {
|
|
330
|
+
const closeError = new Error(`Recommend job dropdown remained open after sticky verification: ${stickyVerification.menu_close.reason || "unknown"}`);
|
|
331
|
+
closeError.sticky_verification = stickyVerification;
|
|
332
|
+
throw closeError;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
attempts.push(compactJobSelectionAttempt({
|
|
336
|
+
ok: true,
|
|
337
|
+
attempt,
|
|
338
|
+
iframeDocumentNodeId,
|
|
339
|
+
selection
|
|
340
|
+
}));
|
|
341
|
+
return {
|
|
342
|
+
job_selection: {
|
|
343
|
+
...selection,
|
|
344
|
+
refresh_attempts: attempts
|
|
345
|
+
},
|
|
346
|
+
root_state: currentRootState,
|
|
347
|
+
attempts
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
lastError = error;
|
|
351
|
+
attempts.push(compactJobSelectionAttempt({
|
|
352
|
+
ok: false,
|
|
353
|
+
attempt,
|
|
354
|
+
iframeDocumentNodeId,
|
|
355
|
+
error
|
|
356
|
+
}));
|
|
357
|
+
if (!isRetryableRecommendJobSelectionError(error) || Date.now() - started >= totalTimeoutMs) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (retryDelayMs > 0) await sleep(retryDelayMs);
|
|
361
|
+
currentRootState = await waitForFreshRecommendRoots(client, {
|
|
362
|
+
timeoutMs: Math.min(10000, Math.max(2000, totalTimeoutMs - (Date.now() - started))),
|
|
363
|
+
intervalMs: 500
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const wrapped = new Error(lastError?.message || "Recommend job selection failed after refresh reload");
|
|
369
|
+
wrapped.cause = lastError;
|
|
370
|
+
wrapped.job_selection_attempts = attempts;
|
|
371
|
+
throw wrapped;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
|
|
375
|
+
maxAttempts = 3,
|
|
376
|
+
retryDelayMs = 1500
|
|
377
|
+
} = {}) {
|
|
378
|
+
const attempts = [];
|
|
379
|
+
let currentRootState = rootState;
|
|
380
|
+
let lastError = null;
|
|
381
|
+
|
|
382
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
383
|
+
try {
|
|
384
|
+
const filter = await selectAndConfirmFirstSafeFilter(
|
|
385
|
+
client,
|
|
386
|
+
currentRootState.iframe.documentNodeId,
|
|
387
|
+
filterOptions
|
|
388
|
+
);
|
|
389
|
+
attempts.push({
|
|
390
|
+
ok: true,
|
|
391
|
+
method: "filter_reapply",
|
|
392
|
+
attempt
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
filter,
|
|
396
|
+
root_state: currentRootState,
|
|
397
|
+
attempts
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
lastError = error;
|
|
401
|
+
attempts.push({
|
|
402
|
+
ok: false,
|
|
403
|
+
method: "filter_reapply",
|
|
404
|
+
reason: "filter_reapply_failed",
|
|
405
|
+
error: compactFilterReapplyError(error),
|
|
406
|
+
error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
|
|
407
|
+
attempt
|
|
408
|
+
});
|
|
409
|
+
if (attempt >= maxAttempts || !isRetryableRecommendFilterReapplyError(error)) {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if (retryDelayMs > 0) await sleep(retryDelayMs);
|
|
413
|
+
currentRootState = await getRecommendRoots(client);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const wrapped = new Error(compactFilterReapplyError(lastError));
|
|
258
418
|
wrapped.cause = lastError;
|
|
259
|
-
wrapped.
|
|
419
|
+
wrapped.filter_reapply_attempts = attempts;
|
|
260
420
|
throw wrapped;
|
|
261
421
|
}
|
|
262
422
|
|
|
263
|
-
|
|
423
|
+
function compactCurrentCityOnlyReapplyError(error) {
|
|
424
|
+
return error?.message || String(error || "Recommend current-city-only reapply failed");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isRetryableRecommendCurrentCityOnlyReapplyError(error) {
|
|
428
|
+
if (isStaleRecommendNodeError(error)) return true;
|
|
429
|
+
const message = compactCurrentCityOnlyReapplyError(error);
|
|
430
|
+
return /current-city|current city|location|popover|trigger|checkbox|stale|did not (?:open|mount|close)/i.test(message)
|
|
431
|
+
&& !/unavailable/i.test(message);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function ensureRefreshCurrentCityOnly(client, rootState, {
|
|
435
|
+
enabled = false,
|
|
264
436
|
maxAttempts = 3,
|
|
265
437
|
retryDelayMs = 1500
|
|
266
438
|
} = {}) {
|
|
@@ -270,18 +442,19 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
|
|
|
270
442
|
|
|
271
443
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
272
444
|
try {
|
|
273
|
-
const
|
|
445
|
+
const result = await ensureRecommendCurrentCityOnly(
|
|
274
446
|
client,
|
|
275
447
|
currentRootState.iframe.documentNodeId,
|
|
276
|
-
|
|
448
|
+
{ enabled }
|
|
277
449
|
);
|
|
278
450
|
attempts.push({
|
|
279
451
|
ok: true,
|
|
280
|
-
method: "
|
|
281
|
-
attempt
|
|
452
|
+
method: "current_city_only_reapply",
|
|
453
|
+
attempt,
|
|
454
|
+
result
|
|
282
455
|
});
|
|
283
456
|
return {
|
|
284
|
-
|
|
457
|
+
current_city_only: result,
|
|
285
458
|
root_state: currentRootState,
|
|
286
459
|
attempts
|
|
287
460
|
};
|
|
@@ -289,12 +462,13 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
|
|
|
289
462
|
lastError = error;
|
|
290
463
|
attempts.push({
|
|
291
464
|
ok: false,
|
|
292
|
-
method: "
|
|
293
|
-
reason: "
|
|
294
|
-
error:
|
|
465
|
+
method: "current_city_only_reapply",
|
|
466
|
+
reason: "current_city_only_reapply_failed",
|
|
467
|
+
error: compactCurrentCityOnlyReapplyError(error),
|
|
468
|
+
error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
|
|
295
469
|
attempt
|
|
296
470
|
});
|
|
297
|
-
if (attempt >= maxAttempts || !
|
|
471
|
+
if (attempt >= maxAttempts || !isRetryableRecommendCurrentCityOnlyReapplyError(error)) {
|
|
298
472
|
break;
|
|
299
473
|
}
|
|
300
474
|
if (retryDelayMs > 0) await sleep(retryDelayMs);
|
|
@@ -302,9 +476,9 @@ async function selectAndConfirmRefreshFilter(client, rootState, filterOptions, {
|
|
|
302
476
|
}
|
|
303
477
|
}
|
|
304
478
|
|
|
305
|
-
const wrapped = new Error(
|
|
479
|
+
const wrapped = new Error(compactCurrentCityOnlyReapplyError(lastError));
|
|
306
480
|
wrapped.cause = lastError;
|
|
307
|
-
wrapped.
|
|
481
|
+
wrapped.current_city_only_attempts = attempts;
|
|
308
482
|
throw wrapped;
|
|
309
483
|
}
|
|
310
484
|
|
|
@@ -313,174 +487,215 @@ async function applyRefreshMethod(client, method, {
|
|
|
313
487
|
pageScope = "recommend",
|
|
314
488
|
fallbackPageScope = "recommend",
|
|
315
489
|
filter = {},
|
|
316
|
-
targetUrl = RECOMMEND_TARGET_URL,
|
|
317
|
-
forceRecentNotView = true,
|
|
318
|
-
cardTimeoutMs = 30000,
|
|
319
|
-
reloadSettleMs = 8000
|
|
320
|
-
} = {}) {
|
|
490
|
+
targetUrl = RECOMMEND_TARGET_URL,
|
|
491
|
+
forceRecentNotView = true,
|
|
492
|
+
cardTimeoutMs = 30000,
|
|
493
|
+
reloadSettleMs = 8000
|
|
494
|
+
} = {}) {
|
|
321
495
|
const started = Date.now();
|
|
322
496
|
let currentRootState = null;
|
|
323
497
|
let jobSelection = null;
|
|
324
498
|
let jobSelectionAttempts = [];
|
|
325
499
|
let pageScopeResult = null;
|
|
500
|
+
let currentCityOnlyResult = null;
|
|
501
|
+
let currentCityOnlyAttempts = [];
|
|
326
502
|
let filterResult = null;
|
|
327
503
|
let filterReapplyAttempts = [];
|
|
328
504
|
let recoverySettle = null;
|
|
329
505
|
try {
|
|
330
506
|
if (method === "page_navigate") {
|
|
331
|
-
await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
|
|
332
|
-
} else {
|
|
333
|
-
await client.Page.reload({ ignoreCache: true });
|
|
334
|
-
}
|
|
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
|
-
}
|
|
342
|
-
currentRootState = await waitForRecommendRoots(client, {
|
|
343
|
-
timeoutMs: Math.max(45000, reloadSettleMs * 6),
|
|
344
|
-
intervalMs: 500
|
|
345
|
-
});
|
|
346
|
-
if (!currentRootState?.iframe?.documentNodeId) {
|
|
347
|
-
throw new Error("Recommend iframe was not ready after refresh reload");
|
|
348
|
-
}
|
|
349
|
-
if (jobLabel) {
|
|
350
|
-
const jobSelectionResult = await selectRecommendJobWithRootRefresh(client, currentRootState, {
|
|
351
|
-
jobLabel,
|
|
352
|
-
settleMs: reloadSettleMs > 10000 ? 12000 : 6000,
|
|
353
|
-
dropdownTimeoutMs: 4000,
|
|
354
|
-
totalTimeoutMs: reloadSettleMs > 10000 ? 45000 : 30000,
|
|
355
|
-
retryDelayMs: 1200
|
|
356
|
-
});
|
|
357
|
-
jobSelection = jobSelectionResult.job_selection;
|
|
358
|
-
jobSelectionAttempts = jobSelectionResult.attempts;
|
|
359
|
-
if (!jobSelection.selected) {
|
|
360
|
-
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
361
|
-
}
|
|
362
|
-
currentRootState = jobSelectionResult.root_state || await getRecommendRoots(client);
|
|
363
|
-
}
|
|
364
|
-
pageScopeResult = await selectRecommendPageScope(
|
|
365
|
-
client,
|
|
366
|
-
currentRootState.iframe.documentNodeId,
|
|
367
|
-
{
|
|
368
|
-
pageScope,
|
|
369
|
-
fallbackScope: fallbackPageScope,
|
|
370
|
-
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
371
|
-
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
372
|
-
}
|
|
507
|
+
await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
|
|
508
|
+
} else {
|
|
509
|
+
await client.Page.reload({ ignoreCache: true });
|
|
510
|
+
}
|
|
511
|
+
recoverySettle = await waitForRecommendRecoverySettle(client, {
|
|
512
|
+
reloadSettleMs,
|
|
513
|
+
timeoutMs: Math.max(45000, reloadSettleMs * 6)
|
|
514
|
+
});
|
|
515
|
+
if (!recoverySettle.ok) {
|
|
516
|
+
throw createRecoverySettleError("recommend", recoverySettle);
|
|
517
|
+
}
|
|
518
|
+
currentRootState = await waitForRecommendRoots(client, {
|
|
519
|
+
timeoutMs: Math.max(45000, reloadSettleMs * 6),
|
|
520
|
+
intervalMs: 500
|
|
521
|
+
});
|
|
522
|
+
if (!currentRootState?.iframe?.documentNodeId) {
|
|
523
|
+
throw new Error("Recommend iframe was not ready after refresh reload");
|
|
524
|
+
}
|
|
525
|
+
if (jobLabel) {
|
|
526
|
+
const jobSelectionResult = await selectRecommendJobWithRootRefresh(client, currentRootState, {
|
|
527
|
+
jobLabel,
|
|
528
|
+
settleMs: reloadSettleMs > 10000 ? 12000 : 6000,
|
|
529
|
+
dropdownTimeoutMs: 4000,
|
|
530
|
+
totalTimeoutMs: reloadSettleMs > 10000 ? 45000 : 30000,
|
|
531
|
+
retryDelayMs: 1200
|
|
532
|
+
});
|
|
533
|
+
jobSelection = jobSelectionResult.job_selection;
|
|
534
|
+
jobSelectionAttempts = jobSelectionResult.attempts;
|
|
535
|
+
if (!jobSelection.selected) {
|
|
536
|
+
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
537
|
+
}
|
|
538
|
+
currentRootState = jobSelectionResult.root_state || await getRecommendRoots(client);
|
|
539
|
+
}
|
|
540
|
+
pageScopeResult = await selectRecommendPageScope(
|
|
541
|
+
client,
|
|
542
|
+
currentRootState.iframe.documentNodeId,
|
|
543
|
+
{
|
|
544
|
+
pageScope,
|
|
545
|
+
fallbackScope: fallbackPageScope,
|
|
546
|
+
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
547
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
548
|
+
}
|
|
373
549
|
);
|
|
374
550
|
if (!pageScopeResult.selected) {
|
|
375
551
|
throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
|
|
376
552
|
}
|
|
377
553
|
currentRootState = await getRecommendRoots(client);
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
554
|
+
const retryDelayMs = Math.max(1200, Math.min(5000, Math.floor((reloadSettleMs || 8000) / 2)));
|
|
555
|
+
const filterStages = await applyRecommendFilterEnvelopeStages(filter, {
|
|
556
|
+
applyCurrentCityOnly: async () => {
|
|
557
|
+
const selection = await ensureRefreshCurrentCityOnly(client, currentRootState, {
|
|
558
|
+
enabled: filter.currentCityOnly === true || filter.current_city_only === true,
|
|
559
|
+
retryDelayMs
|
|
560
|
+
});
|
|
561
|
+
currentCityOnlyAttempts = selection.attempts;
|
|
562
|
+
currentRootState = await getRecommendRoots(client);
|
|
563
|
+
return selection.current_city_only;
|
|
564
|
+
},
|
|
565
|
+
applyFilterPanel: async () => {
|
|
566
|
+
const selection = await selectAndConfirmRefreshFilter(
|
|
567
|
+
client,
|
|
568
|
+
currentRootState,
|
|
569
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
|
|
570
|
+
{ retryDelayMs }
|
|
571
|
+
);
|
|
572
|
+
filterReapplyAttempts = selection.attempts;
|
|
573
|
+
currentRootState = await getRecommendRoots(client);
|
|
574
|
+
return selection.filter;
|
|
384
575
|
}
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
currentRootState = await getRecommendRoots(client);
|
|
576
|
+
});
|
|
577
|
+
currentCityOnlyResult = filterStages.current_city_only;
|
|
578
|
+
filterResult = filterStages.filter;
|
|
389
579
|
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
390
580
|
timeoutMs: cardTimeoutMs,
|
|
391
581
|
intervalMs: 500
|
|
392
582
|
});
|
|
393
583
|
if (!cardNodeIds.length) {
|
|
394
|
-
throw new Error("No recommend candidate cards were found after refresh reload");
|
|
395
|
-
}
|
|
396
|
-
return {
|
|
397
|
-
ok: true,
|
|
398
|
-
method,
|
|
584
|
+
throw new Error("No recommend candidate cards were found after refresh reload");
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
ok: true,
|
|
588
|
+
method,
|
|
399
589
|
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
400
590
|
job_selection: jobSelection,
|
|
401
591
|
job_selection_attempts: jobSelectionAttempts,
|
|
402
592
|
recovery_settle: recoverySettle,
|
|
403
593
|
page_scope: pageScopeResult,
|
|
594
|
+
current_city_only: currentCityOnlyResult,
|
|
595
|
+
current_city_only_attempts: currentCityOnlyAttempts,
|
|
404
596
|
filter: filterResult,
|
|
405
597
|
filter_reapply_attempts: filterReapplyAttempts,
|
|
406
598
|
card_count: cardNodeIds.length,
|
|
407
599
|
root_state: currentRootState,
|
|
408
|
-
forced_recent_not_view: forceRecentNotView,
|
|
600
|
+
forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
|
|
409
601
|
elapsed_ms: Date.now() - started
|
|
410
602
|
};
|
|
411
603
|
} catch (error) {
|
|
412
604
|
return {
|
|
413
605
|
ok: false,
|
|
414
|
-
method,
|
|
606
|
+
method,
|
|
415
607
|
reason: refreshFailureReason(method),
|
|
416
608
|
error: error?.message || String(error),
|
|
609
|
+
error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
|
|
417
610
|
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
418
611
|
job_selection: jobSelection,
|
|
419
612
|
job_selection_attempts: error?.job_selection_attempts || jobSelectionAttempts,
|
|
420
613
|
recovery_settle: error?.recovery_settle || recoverySettle,
|
|
421
614
|
page_scope: pageScopeResult,
|
|
615
|
+
current_city_only: currentCityOnlyResult,
|
|
616
|
+
current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
|
|
422
617
|
filter: filterResult,
|
|
423
618
|
filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
|
|
424
619
|
card_count: 0,
|
|
425
620
|
root_state: currentRootState,
|
|
426
|
-
forced_recent_not_view: forceRecentNotView,
|
|
621
|
+
forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false),
|
|
427
622
|
elapsed_ms: Date.now() - started
|
|
428
623
|
};
|
|
429
624
|
}
|
|
430
625
|
}
|
|
431
626
|
|
|
432
|
-
export async function refreshRecommendListAtEnd(client, {
|
|
433
|
-
rootState = null,
|
|
434
|
-
jobLabel = "",
|
|
435
|
-
pageScope = "recommend",
|
|
436
|
-
fallbackPageScope = "recommend",
|
|
437
|
-
filter = {},
|
|
438
|
-
preferEndRefreshButton = true,
|
|
439
|
-
forceNavigate = false,
|
|
440
|
-
targetUrl = RECOMMEND_TARGET_URL,
|
|
441
|
-
forceRecentNotView = true,
|
|
442
|
-
cardTimeoutMs = 30000,
|
|
443
|
-
buttonSettleMs = 8000,
|
|
444
|
-
reloadSettleMs = 8000
|
|
445
|
-
} = {}) {
|
|
446
|
-
const attempts = [];
|
|
447
|
-
let currentRootState = rootState || null;
|
|
448
|
-
|
|
449
|
-
if (preferEndRefreshButton) {
|
|
450
|
-
currentRootState = currentRootState || await getRecommendRoots(client);
|
|
451
|
-
const buttonResult = await clickRecommendEndRefreshButton(
|
|
452
|
-
client,
|
|
627
|
+
export async function refreshRecommendListAtEnd(client, {
|
|
628
|
+
rootState = null,
|
|
629
|
+
jobLabel = "",
|
|
630
|
+
pageScope = "recommend",
|
|
631
|
+
fallbackPageScope = "recommend",
|
|
632
|
+
filter = {},
|
|
633
|
+
preferEndRefreshButton = true,
|
|
634
|
+
forceNavigate = false,
|
|
635
|
+
targetUrl = RECOMMEND_TARGET_URL,
|
|
636
|
+
forceRecentNotView = true,
|
|
637
|
+
cardTimeoutMs = 30000,
|
|
638
|
+
buttonSettleMs = 8000,
|
|
639
|
+
reloadSettleMs = 8000
|
|
640
|
+
} = {}) {
|
|
641
|
+
const attempts = [];
|
|
642
|
+
let currentRootState = rootState || null;
|
|
643
|
+
|
|
644
|
+
if (preferEndRefreshButton) {
|
|
645
|
+
currentRootState = currentRootState || await getRecommendRoots(client);
|
|
646
|
+
const buttonResult = await clickRecommendEndRefreshButton(
|
|
647
|
+
client,
|
|
453
648
|
currentRootState.iframe.documentNodeId,
|
|
454
649
|
{ settleMs: buttonSettleMs }
|
|
455
650
|
);
|
|
456
651
|
attempts.push(buttonResult);
|
|
457
652
|
if (buttonResult.ok) {
|
|
653
|
+
let pageScopeResult = null;
|
|
654
|
+
let currentCityOnlyResult = null;
|
|
655
|
+
let currentCityOnlyAttempts = [];
|
|
656
|
+
let filterResult = null;
|
|
657
|
+
let filterReapplyAttempts = [];
|
|
458
658
|
try {
|
|
459
659
|
currentRootState = await getRecommendRoots(client);
|
|
460
|
-
|
|
660
|
+
pageScopeResult = await selectRecommendPageScope(
|
|
461
661
|
client,
|
|
462
662
|
currentRootState.iframe.documentNodeId,
|
|
463
663
|
{
|
|
464
664
|
pageScope,
|
|
465
665
|
fallbackScope: fallbackPageScope,
|
|
466
|
-
settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
|
|
467
|
-
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
468
|
-
}
|
|
666
|
+
settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
|
|
667
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
668
|
+
}
|
|
469
669
|
);
|
|
470
670
|
if (!pageScopeResult.selected) {
|
|
471
671
|
throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
|
|
472
672
|
}
|
|
473
673
|
currentRootState = await getRecommendRoots(client);
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
674
|
+
const retryDelayMs = Math.max(1200, Math.min(5000, Math.floor((buttonSettleMs || 8000) / 2)));
|
|
675
|
+
const filterStages = await applyRecommendFilterEnvelopeStages(filter, {
|
|
676
|
+
applyCurrentCityOnly: async () => {
|
|
677
|
+
const selection = await ensureRefreshCurrentCityOnly(client, currentRootState, {
|
|
678
|
+
enabled: filter.currentCityOnly === true || filter.current_city_only === true,
|
|
679
|
+
retryDelayMs
|
|
680
|
+
});
|
|
681
|
+
currentCityOnlyAttempts = selection.attempts;
|
|
682
|
+
currentRootState = await getRecommendRoots(client);
|
|
683
|
+
return selection.current_city_only;
|
|
684
|
+
},
|
|
685
|
+
applyFilterPanel: async () => {
|
|
686
|
+
const selection = await selectAndConfirmRefreshFilter(
|
|
687
|
+
client,
|
|
688
|
+
currentRootState,
|
|
689
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView }),
|
|
690
|
+
{ retryDelayMs }
|
|
691
|
+
);
|
|
692
|
+
filterReapplyAttempts = selection.attempts;
|
|
693
|
+
currentRootState = await getRecommendRoots(client);
|
|
694
|
+
return selection.filter;
|
|
480
695
|
}
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
|
|
696
|
+
});
|
|
697
|
+
currentCityOnlyResult = filterStages.current_city_only;
|
|
698
|
+
filterResult = filterStages.filter;
|
|
484
699
|
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
485
700
|
timeoutMs: cardTimeoutMs,
|
|
486
701
|
intervalMs: 500
|
|
@@ -490,65 +705,74 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
490
705
|
method: "end_refresh_button",
|
|
491
706
|
attempts,
|
|
492
707
|
page_scope: pageScopeResult,
|
|
708
|
+
current_city_only: currentCityOnlyResult,
|
|
709
|
+
current_city_only_attempts: currentCityOnlyAttempts,
|
|
493
710
|
filter: filterResult,
|
|
494
|
-
filter_reapply_attempts:
|
|
711
|
+
filter_reapply_attempts: filterReapplyAttempts,
|
|
495
712
|
card_count: cardNodeIds.length,
|
|
496
713
|
root_state: currentRootState,
|
|
497
|
-
forced_recent_not_view: forceRecentNotView
|
|
714
|
+
forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
|
|
498
715
|
};
|
|
499
716
|
} catch (error) {
|
|
500
717
|
attempts.push({
|
|
501
718
|
ok: false,
|
|
502
719
|
method: "end_refresh_button_after_click",
|
|
503
720
|
reason: "end_refresh_reapply_failed",
|
|
504
|
-
error: error?.message || String(error)
|
|
721
|
+
error: error?.message || String(error),
|
|
722
|
+
error_diagnostic: compactRecommendRefreshErrorDiagnostic(error),
|
|
723
|
+
page_scope: pageScopeResult,
|
|
724
|
+
current_city_only: currentCityOnlyResult,
|
|
725
|
+
current_city_only_attempts: error?.current_city_only_attempts || currentCityOnlyAttempts,
|
|
726
|
+
filter: filterResult,
|
|
727
|
+
filter_reapply_attempts: error?.filter_reapply_attempts || filterReapplyAttempts,
|
|
728
|
+
forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
|
|
505
729
|
});
|
|
506
730
|
}
|
|
507
731
|
}
|
|
508
732
|
}
|
|
509
733
|
|
|
510
|
-
const fallbackMethods = [];
|
|
511
|
-
if (forceNavigate && typeof client?.Page?.navigate === "function") {
|
|
512
|
-
fallbackMethods.push("page_navigate");
|
|
513
|
-
}
|
|
514
|
-
if (typeof client?.Page?.reload === "function") {
|
|
515
|
-
fallbackMethods.push("page_reload");
|
|
516
|
-
}
|
|
517
|
-
if (!fallbackMethods.length) {
|
|
518
|
-
fallbackMethods.push("page_reload");
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
let lastRefreshResult = null;
|
|
522
|
-
for (const method of fallbackMethods) {
|
|
523
|
-
const refreshResult = await applyRefreshMethod(client, method, {
|
|
524
|
-
jobLabel,
|
|
525
|
-
pageScope,
|
|
526
|
-
fallbackPageScope,
|
|
527
|
-
filter,
|
|
528
|
-
targetUrl,
|
|
529
|
-
forceRecentNotView,
|
|
530
|
-
cardTimeoutMs,
|
|
531
|
-
reloadSettleMs
|
|
532
|
-
});
|
|
533
|
-
if (refreshResult.ok) {
|
|
534
|
-
return {
|
|
535
|
-
...refreshResult,
|
|
536
|
-
attempts
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
attempts.push(refreshResult);
|
|
540
|
-
lastRefreshResult = refreshResult;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return {
|
|
544
|
-
...(lastRefreshResult || {
|
|
545
|
-
ok: false,
|
|
734
|
+
const fallbackMethods = [];
|
|
735
|
+
if (forceNavigate && typeof client?.Page?.navigate === "function") {
|
|
736
|
+
fallbackMethods.push("page_navigate");
|
|
737
|
+
}
|
|
738
|
+
if (typeof client?.Page?.reload === "function") {
|
|
739
|
+
fallbackMethods.push("page_reload");
|
|
740
|
+
}
|
|
741
|
+
if (!fallbackMethods.length) {
|
|
742
|
+
fallbackMethods.push("page_reload");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let lastRefreshResult = null;
|
|
746
|
+
for (const method of fallbackMethods) {
|
|
747
|
+
const refreshResult = await applyRefreshMethod(client, method, {
|
|
748
|
+
jobLabel,
|
|
749
|
+
pageScope,
|
|
750
|
+
fallbackPageScope,
|
|
751
|
+
filter,
|
|
752
|
+
targetUrl,
|
|
753
|
+
forceRecentNotView,
|
|
754
|
+
cardTimeoutMs,
|
|
755
|
+
reloadSettleMs
|
|
756
|
+
});
|
|
757
|
+
if (refreshResult.ok) {
|
|
758
|
+
return {
|
|
759
|
+
...refreshResult,
|
|
760
|
+
attempts
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
attempts.push(refreshResult);
|
|
764
|
+
lastRefreshResult = refreshResult;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
...(lastRefreshResult || {
|
|
769
|
+
ok: false,
|
|
546
770
|
method: fallbackMethods[fallbackMethods.length - 1] || "page_reload",
|
|
547
771
|
reason: "refresh_failed",
|
|
548
772
|
error: "Recommend refresh did not run",
|
|
549
773
|
card_count: 0,
|
|
550
774
|
root_state: currentRootState,
|
|
551
|
-
forced_recent_not_view: forceRecentNotView
|
|
775
|
+
forced_recent_not_view: Boolean(forceRecentNotView && filter.enabled !== false)
|
|
552
776
|
}),
|
|
553
777
|
attempts
|
|
554
778
|
};
|