@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0
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 +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clickNodeCenter,
|
|
3
|
+
getAttributesMap,
|
|
4
|
+
getNodeBox,
|
|
5
|
+
getOuterHTML,
|
|
6
|
+
pressKey,
|
|
7
|
+
querySelectorAll,
|
|
8
|
+
sleep,
|
|
9
|
+
waitForSelector
|
|
10
|
+
} from "../../core/browser/index.js";
|
|
11
|
+
import {
|
|
12
|
+
htmlToText,
|
|
13
|
+
normalizeText
|
|
14
|
+
} from "../../core/screening/index.js";
|
|
15
|
+
|
|
16
|
+
export const RECOMMEND_JOB_SELECTORS = Object.freeze({
|
|
17
|
+
trigger: ".job-selecter-wrap, [class*=\"job-selecter-wrap\"], .ui-dropmenu",
|
|
18
|
+
option: ".job-selecter-options .job-item, .job-list .job-item, .job-item",
|
|
19
|
+
current: ".job-selecter-options .job-item.curr, .job-list .job-item.curr, .job-item.curr"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function normalizeJobText(value) {
|
|
23
|
+
return normalizeText(value).replace(/\s+/g, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function trimSalarySuffix(label) {
|
|
27
|
+
return normalizeText(label)
|
|
28
|
+
.replace(/\s+(?:\d+(?:-\d+)?K|面议|\d+-\d+元\/天).*$/i, "")
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function jobLabelMatches(optionLabel, targetLabel) {
|
|
33
|
+
const option = normalizeJobText(optionLabel);
|
|
34
|
+
const target = normalizeJobText(targetLabel);
|
|
35
|
+
if (!option || !target) return false;
|
|
36
|
+
return option === target
|
|
37
|
+
|| option.startsWith(target)
|
|
38
|
+
|| normalizeJobText(trimSalarySuffix(optionLabel)) === target;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isVisibleBox(box) {
|
|
42
|
+
return Boolean(box && box.rect.width > 4 && box.rect.height > 4);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readJobOption(client, nodeId, index) {
|
|
46
|
+
const [attributes, outerHTML] = await Promise.all([
|
|
47
|
+
getAttributesMap(client, nodeId),
|
|
48
|
+
getOuterHTML(client, nodeId)
|
|
49
|
+
]);
|
|
50
|
+
const label = normalizeText(htmlToText(outerHTML));
|
|
51
|
+
let box = null;
|
|
52
|
+
try {
|
|
53
|
+
box = await getNodeBox(client, nodeId);
|
|
54
|
+
} catch {}
|
|
55
|
+
const className = attributes.class || "";
|
|
56
|
+
return {
|
|
57
|
+
node_id: nodeId,
|
|
58
|
+
index,
|
|
59
|
+
label,
|
|
60
|
+
label_without_salary: trimSalarySuffix(label),
|
|
61
|
+
class_name: className,
|
|
62
|
+
current: /\bcurr\b|\bactive\b|\bselected\b/.test(className),
|
|
63
|
+
visible: isVisibleBox(box),
|
|
64
|
+
center: box?.center || null,
|
|
65
|
+
rect: box?.rect || null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function findRecommendJobTrigger(client, frameNodeId) {
|
|
70
|
+
const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_JOB_SELECTORS.trigger);
|
|
71
|
+
for (const nodeId of nodeIds) {
|
|
72
|
+
try {
|
|
73
|
+
const box = await getNodeBox(client, nodeId);
|
|
74
|
+
if (isVisibleBox(box)) {
|
|
75
|
+
return {
|
|
76
|
+
node_id: nodeId,
|
|
77
|
+
center: box.center,
|
|
78
|
+
rect: box.rect
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function openRecommendJobDropdown(client, frameNodeId, {
|
|
87
|
+
timeoutMs = 4000
|
|
88
|
+
} = {}) {
|
|
89
|
+
const trigger = await findRecommendJobTrigger(client, frameNodeId);
|
|
90
|
+
if (!trigger) {
|
|
91
|
+
throw new Error("Recommend job trigger was not found");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let optionNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_JOB_SELECTORS.option, {
|
|
95
|
+
timeoutMs: 300,
|
|
96
|
+
intervalMs: 100
|
|
97
|
+
});
|
|
98
|
+
if (optionNodeId) {
|
|
99
|
+
const options = await listRecommendJobOptions(client, frameNodeId, { openDropdown: false });
|
|
100
|
+
if (options.some((option) => option.visible)) {
|
|
101
|
+
return {
|
|
102
|
+
opened: true,
|
|
103
|
+
already_open: true,
|
|
104
|
+
trigger,
|
|
105
|
+
options
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await clickNodeCenter(client, trigger.node_id);
|
|
111
|
+
optionNodeId = await waitForSelector(client, frameNodeId, RECOMMEND_JOB_SELECTORS.option, {
|
|
112
|
+
timeoutMs,
|
|
113
|
+
intervalMs: 200
|
|
114
|
+
});
|
|
115
|
+
if (!optionNodeId) {
|
|
116
|
+
throw new Error("Recommend job dropdown did not mount options after trigger click");
|
|
117
|
+
}
|
|
118
|
+
const options = await listRecommendJobOptions(client, frameNodeId, { openDropdown: false });
|
|
119
|
+
return {
|
|
120
|
+
opened: true,
|
|
121
|
+
already_open: false,
|
|
122
|
+
trigger,
|
|
123
|
+
options
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function listRecommendJobOptions(client, frameNodeId, {
|
|
128
|
+
openDropdown = true
|
|
129
|
+
} = {}) {
|
|
130
|
+
if (openDropdown) {
|
|
131
|
+
await openRecommendJobDropdown(client, frameNodeId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const nodeIds = await querySelectorAll(client, frameNodeId, RECOMMEND_JOB_SELECTORS.option);
|
|
135
|
+
const options = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (let index = 0; index < nodeIds.length; index += 1) {
|
|
138
|
+
const nodeId = nodeIds[index];
|
|
139
|
+
if (seen.has(nodeId)) continue;
|
|
140
|
+
seen.add(nodeId);
|
|
141
|
+
const option = await readJobOption(client, nodeId, index);
|
|
142
|
+
if (!option.label) continue;
|
|
143
|
+
if (option.label.length > 120) continue;
|
|
144
|
+
options.push(option);
|
|
145
|
+
}
|
|
146
|
+
return options;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function closeRecommendJobDropdown(client) {
|
|
150
|
+
await pressKey(client, "Escape", {
|
|
151
|
+
code: "Escape",
|
|
152
|
+
windowsVirtualKeyCode: 27,
|
|
153
|
+
nativeVirtualKeyCode: 27
|
|
154
|
+
});
|
|
155
|
+
await sleep(300);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function selectRecommendJob(client, frameNodeId, {
|
|
159
|
+
jobLabel = "",
|
|
160
|
+
settleMs = 6000
|
|
161
|
+
} = {}) {
|
|
162
|
+
const target = normalizeText(jobLabel);
|
|
163
|
+
if (!target) {
|
|
164
|
+
return {
|
|
165
|
+
requested: "",
|
|
166
|
+
selected: false,
|
|
167
|
+
reason: "no_job_requested",
|
|
168
|
+
options: []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const opened = await openRecommendJobDropdown(client, frameNodeId);
|
|
173
|
+
const options = opened.options.length
|
|
174
|
+
? opened.options
|
|
175
|
+
: await listRecommendJobOptions(client, frameNodeId, { openDropdown: false });
|
|
176
|
+
const visibleOptions = options.filter((option) => option.visible);
|
|
177
|
+
const match = visibleOptions.find((option) => jobLabelMatches(option.label, target))
|
|
178
|
+
|| options.find((option) => jobLabelMatches(option.label, target));
|
|
179
|
+
|
|
180
|
+
if (!match) {
|
|
181
|
+
await closeRecommendJobDropdown(client);
|
|
182
|
+
return {
|
|
183
|
+
requested: target,
|
|
184
|
+
selected: false,
|
|
185
|
+
reason: "job_not_found",
|
|
186
|
+
options: options.map(compactJobOption)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (match.current) {
|
|
191
|
+
await closeRecommendJobDropdown(client);
|
|
192
|
+
return {
|
|
193
|
+
requested: target,
|
|
194
|
+
selected: true,
|
|
195
|
+
already_current: true,
|
|
196
|
+
selected_option: compactJobOption(match),
|
|
197
|
+
options: options.map(compactJobOption)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!match.center) {
|
|
202
|
+
await closeRecommendJobDropdown(client);
|
|
203
|
+
throw new Error(`Matched recommend job has no clickable center: ${match.label}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const clickedBox = await clickNodeCenter(client, match.node_id);
|
|
207
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
208
|
+
return {
|
|
209
|
+
requested: target,
|
|
210
|
+
selected: true,
|
|
211
|
+
already_current: false,
|
|
212
|
+
selected_option: compactJobOption(match),
|
|
213
|
+
click_box: {
|
|
214
|
+
center: clickedBox.center,
|
|
215
|
+
rect: clickedBox.rect
|
|
216
|
+
},
|
|
217
|
+
options: options.map(compactJobOption)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function compactJobOption(option) {
|
|
222
|
+
return {
|
|
223
|
+
label: option.label,
|
|
224
|
+
label_without_salary: option.label_without_salary,
|
|
225
|
+
current: Boolean(option.current),
|
|
226
|
+
visible: Boolean(option.visible),
|
|
227
|
+
class_name: option.class_name,
|
|
228
|
+
node_id: option.node_id,
|
|
229
|
+
center: option.center,
|
|
230
|
+
rect: option.rect
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { sleep } from "../../core/browser/index.js";
|
|
2
|
+
import {
|
|
3
|
+
clickRecommendEndRefreshButton,
|
|
4
|
+
waitForRecommendCardNodeIds
|
|
5
|
+
} from "./cards.js";
|
|
6
|
+
import {
|
|
7
|
+
RECOMMEND_RECENT_NOT_VIEW_LABEL
|
|
8
|
+
} from "./constants.js";
|
|
9
|
+
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
10
|
+
import { selectRecommendJob } from "./jobs.js";
|
|
11
|
+
import { selectRecommendPageScope } from "./scopes.js";
|
|
12
|
+
import {
|
|
13
|
+
getRecommendRoots,
|
|
14
|
+
waitForRecommendRoots
|
|
15
|
+
} from "./roots.js";
|
|
16
|
+
|
|
17
|
+
function normalizeLabels(labels = []) {
|
|
18
|
+
return labels.map((label) => String(label || "").trim()).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeFilterGroup(spec = {}) {
|
|
22
|
+
return {
|
|
23
|
+
group: String(spec.group || "").trim(),
|
|
24
|
+
labels: normalizeLabels(spec.labels || spec.filterLabels || []),
|
|
25
|
+
selectAllLabels: spec.selectAllLabels !== false
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildRecommendFilterGroups(filter = {}, {
|
|
30
|
+
forceRecentNotView = false
|
|
31
|
+
} = {}) {
|
|
32
|
+
const groups = [];
|
|
33
|
+
const sourceGroups = Array.isArray(filter.filterGroups)
|
|
34
|
+
? filter.filterGroups
|
|
35
|
+
: Array.isArray(filter.groups)
|
|
36
|
+
? filter.groups
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
for (const spec of sourceGroups) {
|
|
40
|
+
const group = normalizeFilterGroup(spec);
|
|
41
|
+
if (group.group || group.labels.length) groups.push(group);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rootGroup = normalizeFilterGroup(filter);
|
|
45
|
+
if ((rootGroup.group || rootGroup.labels.length) && !groups.length) {
|
|
46
|
+
groups.push(rootGroup);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (forceRecentNotView) {
|
|
50
|
+
const recentGroup = groups.find((item) => item.group === "recentNotView");
|
|
51
|
+
if (recentGroup) {
|
|
52
|
+
if (!recentGroup.labels.some((label) => label.replace(/\s+/g, "") === RECOMMEND_RECENT_NOT_VIEW_LABEL)) {
|
|
53
|
+
recentGroup.labels.push(RECOMMEND_RECENT_NOT_VIEW_LABEL);
|
|
54
|
+
}
|
|
55
|
+
recentGroup.selectAllLabels = true;
|
|
56
|
+
} else {
|
|
57
|
+
groups.unshift({
|
|
58
|
+
group: "recentNotView",
|
|
59
|
+
labels: [RECOMMEND_RECENT_NOT_VIEW_LABEL],
|
|
60
|
+
selectAllLabels: true
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return groups;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildRecommendFilterSelectionOptions(filter = {}, {
|
|
69
|
+
forceRecentNotView = false
|
|
70
|
+
} = {}) {
|
|
71
|
+
const filterGroups = buildRecommendFilterGroups(filter, { forceRecentNotView });
|
|
72
|
+
if (filterGroups.length > 1 || forceRecentNotView || Array.isArray(filter.filterGroups) || Array.isArray(filter.groups)) {
|
|
73
|
+
return { filterGroups };
|
|
74
|
+
}
|
|
75
|
+
const [singleGroup] = filterGroups;
|
|
76
|
+
if (singleGroup) {
|
|
77
|
+
return {
|
|
78
|
+
group: singleGroup.group,
|
|
79
|
+
labels: singleGroup.labels,
|
|
80
|
+
selectAllLabels: singleGroup.selectAllLabels
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
group: filter.group || "",
|
|
85
|
+
labels: normalizeLabels(filter.labels || filter.filterLabels || []),
|
|
86
|
+
selectAllLabels: filter.selectAllLabels !== false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function refreshRecommendListAtEnd(client, {
|
|
91
|
+
rootState = null,
|
|
92
|
+
jobLabel = "",
|
|
93
|
+
pageScope = "recommend",
|
|
94
|
+
fallbackPageScope = "recommend",
|
|
95
|
+
filter = {},
|
|
96
|
+
preferEndRefreshButton = true,
|
|
97
|
+
forceRecentNotView = true,
|
|
98
|
+
cardTimeoutMs = 30000,
|
|
99
|
+
buttonSettleMs = 8000,
|
|
100
|
+
reloadSettleMs = 8000
|
|
101
|
+
} = {}) {
|
|
102
|
+
const attempts = [];
|
|
103
|
+
let currentRootState = rootState || await getRecommendRoots(client);
|
|
104
|
+
|
|
105
|
+
if (preferEndRefreshButton) {
|
|
106
|
+
const buttonResult = await clickRecommendEndRefreshButton(
|
|
107
|
+
client,
|
|
108
|
+
currentRootState.iframe.documentNodeId,
|
|
109
|
+
{ settleMs: buttonSettleMs }
|
|
110
|
+
);
|
|
111
|
+
attempts.push(buttonResult);
|
|
112
|
+
if (buttonResult.ok) {
|
|
113
|
+
currentRootState = await getRecommendRoots(client);
|
|
114
|
+
const pageScopeResult = await selectRecommendPageScope(
|
|
115
|
+
client,
|
|
116
|
+
currentRootState.iframe.documentNodeId,
|
|
117
|
+
{
|
|
118
|
+
pageScope,
|
|
119
|
+
fallbackScope: fallbackPageScope,
|
|
120
|
+
settleMs: buttonSettleMs > 10000 ? 3000 : 1200,
|
|
121
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
if (!pageScopeResult.selected) {
|
|
125
|
+
throw new Error(`Recommend page scope was not selected after end refresh: ${pageScopeResult.reason || pageScope}`);
|
|
126
|
+
}
|
|
127
|
+
currentRootState = await getRecommendRoots(client);
|
|
128
|
+
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
129
|
+
client,
|
|
130
|
+
currentRootState.iframe.documentNodeId,
|
|
131
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
132
|
+
);
|
|
133
|
+
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
134
|
+
timeoutMs: cardTimeoutMs,
|
|
135
|
+
intervalMs: 500
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
ok: cardNodeIds.length > 0,
|
|
139
|
+
method: "end_refresh_button",
|
|
140
|
+
attempts,
|
|
141
|
+
page_scope: pageScopeResult,
|
|
142
|
+
filter: filterResult,
|
|
143
|
+
card_count: cardNodeIds.length,
|
|
144
|
+
root_state: currentRootState,
|
|
145
|
+
forced_recent_not_view: forceRecentNotView
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await client.Page.reload({ ignoreCache: true });
|
|
151
|
+
if (reloadSettleMs > 0) await sleep(reloadSettleMs);
|
|
152
|
+
currentRootState = await waitForRecommendRoots(client, {
|
|
153
|
+
timeoutMs: Math.max(30000, reloadSettleMs * 4),
|
|
154
|
+
intervalMs: 500
|
|
155
|
+
});
|
|
156
|
+
if (!currentRootState?.iframe?.documentNodeId) {
|
|
157
|
+
throw new Error("Recommend iframe was not ready after refresh reload");
|
|
158
|
+
}
|
|
159
|
+
let jobSelection = null;
|
|
160
|
+
if (jobLabel) {
|
|
161
|
+
jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
|
|
162
|
+
jobLabel,
|
|
163
|
+
settleMs: reloadSettleMs > 10000 ? 12000 : 6000
|
|
164
|
+
});
|
|
165
|
+
if (!jobSelection.selected) {
|
|
166
|
+
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
167
|
+
}
|
|
168
|
+
currentRootState = await getRecommendRoots(client);
|
|
169
|
+
}
|
|
170
|
+
const pageScopeResult = await selectRecommendPageScope(
|
|
171
|
+
client,
|
|
172
|
+
currentRootState.iframe.documentNodeId,
|
|
173
|
+
{
|
|
174
|
+
pageScope,
|
|
175
|
+
fallbackScope: fallbackPageScope,
|
|
176
|
+
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
177
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
if (!pageScopeResult.selected) {
|
|
181
|
+
throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
|
|
182
|
+
}
|
|
183
|
+
currentRootState = await getRecommendRoots(client);
|
|
184
|
+
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
185
|
+
client,
|
|
186
|
+
currentRootState.iframe.documentNodeId,
|
|
187
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
188
|
+
);
|
|
189
|
+
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
190
|
+
timeoutMs: cardTimeoutMs,
|
|
191
|
+
intervalMs: 500
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
ok: cardNodeIds.length > 0,
|
|
195
|
+
method: "page_reload",
|
|
196
|
+
attempts,
|
|
197
|
+
job_selection: jobSelection,
|
|
198
|
+
page_scope: pageScopeResult,
|
|
199
|
+
filter: filterResult,
|
|
200
|
+
card_count: cardNodeIds.length,
|
|
201
|
+
root_state: currentRootState,
|
|
202
|
+
forced_recent_not_view: forceRecentNotView
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findIframeDocument,
|
|
3
|
+
getDocumentRoot,
|
|
4
|
+
querySelector,
|
|
5
|
+
sleep
|
|
6
|
+
} from "../../core/browser/index.js";
|
|
7
|
+
import { RECOMMEND_IFRAME_SELECTORS } from "./constants.js";
|
|
8
|
+
|
|
9
|
+
export async function getRecommendRoots(client, {
|
|
10
|
+
iframeSelectors = RECOMMEND_IFRAME_SELECTORS,
|
|
11
|
+
requireFrame = true
|
|
12
|
+
} = {}) {
|
|
13
|
+
const topRoot = await getDocumentRoot(client);
|
|
14
|
+
const iframe = await findIframeDocument(client, topRoot.nodeId, iframeSelectors);
|
|
15
|
+
if (!iframe && requireFrame) {
|
|
16
|
+
throw new Error("recommendFrame iframe was not found");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
topRoot,
|
|
21
|
+
iframe,
|
|
22
|
+
roots: [
|
|
23
|
+
{ name: "top", nodeId: topRoot.nodeId },
|
|
24
|
+
iframe ? { name: "recommend-frame", nodeId: iframe.documentNodeId } : null
|
|
25
|
+
].filter(Boolean),
|
|
26
|
+
rootNodes: {
|
|
27
|
+
top: topRoot.nodeId,
|
|
28
|
+
frame: iframe?.documentNodeId || 0
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function waitForRecommendRoots(client, {
|
|
34
|
+
timeoutMs = 10000,
|
|
35
|
+
intervalMs = 250,
|
|
36
|
+
iframeSelectors = RECOMMEND_IFRAME_SELECTORS
|
|
37
|
+
} = {}) {
|
|
38
|
+
const started = Date.now();
|
|
39
|
+
let lastState = null;
|
|
40
|
+
while (Date.now() - started <= timeoutMs) {
|
|
41
|
+
try {
|
|
42
|
+
lastState = await getRecommendRoots(client, {
|
|
43
|
+
iframeSelectors,
|
|
44
|
+
requireFrame: false
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
lastState = {
|
|
48
|
+
error: error?.message || String(error),
|
|
49
|
+
roots: [],
|
|
50
|
+
rootNodes: {
|
|
51
|
+
top: 0,
|
|
52
|
+
frame: 0
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (lastState.iframe?.documentNodeId) return lastState;
|
|
57
|
+
await sleep(intervalMs);
|
|
58
|
+
}
|
|
59
|
+
return lastState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function queryFirstAcrossRoots(client, roots, selectors) {
|
|
63
|
+
for (const root of roots) {
|
|
64
|
+
if (!root?.nodeId) continue;
|
|
65
|
+
for (const selector of selectors) {
|
|
66
|
+
const nodeId = await querySelector(client, root.nodeId, selector);
|
|
67
|
+
if (nodeId) {
|
|
68
|
+
return {
|
|
69
|
+
root: root.name,
|
|
70
|
+
root_node_id: root.nodeId,
|
|
71
|
+
selector,
|
|
72
|
+
node_id: nodeId
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|