@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1
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 +86 -33
- package/package.json +62 -9
- package/skills/boss-chat/SKILL.md +5 -4
- package/skills/boss-recommend-pipeline/SKILL.md +21 -31
- package/skills/boss-recruit-pipeline/README.md +17 -0
- package/skills/boss-recruit-pipeline/SKILL.md +55 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1254 -225
- 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 +66 -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 +67 -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 -7072
- 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 -2423
- 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,334 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clickNodeCenter,
|
|
3
|
+
clickPoint,
|
|
4
|
+
getAttributesMap,
|
|
5
|
+
getNodeBox,
|
|
6
|
+
getOuterHTML,
|
|
7
|
+
querySelector,
|
|
8
|
+
querySelectorAll,
|
|
9
|
+
sleep
|
|
10
|
+
} from "../../core/browser/index.js";
|
|
11
|
+
import {
|
|
12
|
+
htmlToText,
|
|
13
|
+
normalizeText
|
|
14
|
+
} from "../../core/screening/index.js";
|
|
15
|
+
import {
|
|
16
|
+
CHAT_JOB_FALLBACK_SELECTORS,
|
|
17
|
+
CHAT_JOB_LABEL_SELECTORS,
|
|
18
|
+
CHAT_JOB_OPTION_SELECTORS,
|
|
19
|
+
CHAT_JOB_TRIGGER_SELECTORS
|
|
20
|
+
} from "./constants.js";
|
|
21
|
+
import { getChatRoots } from "./roots.js";
|
|
22
|
+
|
|
23
|
+
function isActiveClass(className = "") {
|
|
24
|
+
return /\b(active|selected|current)\b/i.test(String(className || ""));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeJobText(value) {
|
|
28
|
+
return normalizeText(value).replace(/\s+_/g, " _").replace(/_\s+/g, "_ ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function freshTopRootNodeId(client, fallbackNodeId) {
|
|
32
|
+
try {
|
|
33
|
+
const rootState = await getChatRoots(client);
|
|
34
|
+
return rootState.rootNodes.top || fallbackNodeId;
|
|
35
|
+
} catch {
|
|
36
|
+
return fallbackNodeId;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function safeQuerySelector(client, rootNodeId, selector) {
|
|
41
|
+
try {
|
|
42
|
+
return await querySelector(client, rootNodeId, selector);
|
|
43
|
+
} catch {
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function safeQuerySelectorAll(client, rootNodeId, selector) {
|
|
49
|
+
try {
|
|
50
|
+
return await querySelectorAll(client, rootNodeId, selector);
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readNodeText(client, nodeId) {
|
|
57
|
+
const outerHTML = await getOuterHTML(client, nodeId);
|
|
58
|
+
return {
|
|
59
|
+
outerHTML,
|
|
60
|
+
text: normalizeJobText(htmlToText(outerHTML))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readSelectedJobLabel(client, rootNodeId) {
|
|
65
|
+
for (const selector of CHAT_JOB_LABEL_SELECTORS) {
|
|
66
|
+
const nodeId = await safeQuerySelector(client, rootNodeId, selector);
|
|
67
|
+
if (!nodeId) continue;
|
|
68
|
+
try {
|
|
69
|
+
const { text } = await readNodeText(client, nodeId);
|
|
70
|
+
if (text) return { selector, label: text };
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { selector: "", label: "" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readOptionNode(client, nodeId, index, { selector, source }) {
|
|
79
|
+
const [attributes, textResult] = await Promise.all([
|
|
80
|
+
getAttributesMap(client, nodeId),
|
|
81
|
+
readNodeText(client, nodeId)
|
|
82
|
+
]);
|
|
83
|
+
const label = normalizeJobText(attributes.title || textResult.text);
|
|
84
|
+
if (!label) return null;
|
|
85
|
+
const rawValue = normalizeText(attributes.value || attributes["data-value"] || attributes["data-id"] || "");
|
|
86
|
+
return {
|
|
87
|
+
node_id: nodeId,
|
|
88
|
+
index,
|
|
89
|
+
label,
|
|
90
|
+
title: label,
|
|
91
|
+
value: rawValue || label,
|
|
92
|
+
active: isActiveClass(attributes.class),
|
|
93
|
+
is_all: rawValue === "-1" || /^(全部职位|全部岗位|全部)$/u.test(label),
|
|
94
|
+
source,
|
|
95
|
+
selector
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readClickableOptionNode(client, nodeId, index, { selector, source }) {
|
|
100
|
+
const option = await readOptionNode(client, nodeId, index, { selector, source });
|
|
101
|
+
if (!option) return null;
|
|
102
|
+
try {
|
|
103
|
+
const box = await getNodeBox(client, nodeId);
|
|
104
|
+
option.center = box.center;
|
|
105
|
+
option.rect = box.rect;
|
|
106
|
+
option.visible = box.rect.width > 2 && box.rect.height > 2;
|
|
107
|
+
} catch {
|
|
108
|
+
option.center = null;
|
|
109
|
+
option.rect = null;
|
|
110
|
+
option.visible = false;
|
|
111
|
+
}
|
|
112
|
+
return option;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readOptionsForSelector(client, rootNodeId, selector, { source }) {
|
|
116
|
+
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
117
|
+
const options = [];
|
|
118
|
+
for (const nodeId of nodeIds) {
|
|
119
|
+
let option = null;
|
|
120
|
+
try {
|
|
121
|
+
option = await readClickableOptionNode(client, nodeId, options.length + 1, {
|
|
122
|
+
selector,
|
|
123
|
+
source
|
|
124
|
+
});
|
|
125
|
+
} catch {
|
|
126
|
+
option = null;
|
|
127
|
+
}
|
|
128
|
+
if (option) options.push(option);
|
|
129
|
+
}
|
|
130
|
+
return options;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function dedupeJobOptions(options = []) {
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
const deduped = [];
|
|
136
|
+
for (const option of options) {
|
|
137
|
+
const key = `${normalizeText(option.value).toLowerCase()}|${normalizeText(option.label).toLowerCase()}`;
|
|
138
|
+
if (seen.has(key)) continue;
|
|
139
|
+
seen.add(key);
|
|
140
|
+
deduped.push({
|
|
141
|
+
...option,
|
|
142
|
+
index: deduped.length + 1
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return deduped;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function readChatJobOptions(client, rootNodeId, {
|
|
149
|
+
timeoutMs = 12000,
|
|
150
|
+
intervalMs = 300
|
|
151
|
+
} = {}) {
|
|
152
|
+
const started = Date.now();
|
|
153
|
+
let selected = { selector: "", label: "" };
|
|
154
|
+
let lastPrimary = {
|
|
155
|
+
selector: "",
|
|
156
|
+
source: "chat-job-list",
|
|
157
|
+
options: []
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
while (Date.now() - started <= timeoutMs) {
|
|
161
|
+
selected = await readSelectedJobLabel(client, rootNodeId);
|
|
162
|
+
for (const selector of CHAT_JOB_OPTION_SELECTORS) {
|
|
163
|
+
const options = await readOptionsForSelector(client, rootNodeId, selector, {
|
|
164
|
+
source: "chat-job-list"
|
|
165
|
+
});
|
|
166
|
+
if (options.length) {
|
|
167
|
+
lastPrimary = {
|
|
168
|
+
selector,
|
|
169
|
+
source: "chat-job-list",
|
|
170
|
+
options: dedupeJobOptions(options)
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
selector,
|
|
174
|
+
source: "chat-job-list",
|
|
175
|
+
selected_label: selected.label || "",
|
|
176
|
+
selected_selector: selected.selector || "",
|
|
177
|
+
job_options: lastPrimary.options
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
await sleep(intervalMs);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const fallbackOptions = [];
|
|
185
|
+
for (const selector of CHAT_JOB_FALLBACK_SELECTORS) {
|
|
186
|
+
const options = await readOptionsForSelector(client, rootNodeId, selector, {
|
|
187
|
+
source: "conversation-source-job"
|
|
188
|
+
});
|
|
189
|
+
fallbackOptions.push(...options);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const dedupedFallback = dedupeJobOptions(fallbackOptions);
|
|
193
|
+
if (dedupedFallback.length) {
|
|
194
|
+
return {
|
|
195
|
+
selector: CHAT_JOB_FALLBACK_SELECTORS.join(", "),
|
|
196
|
+
source: "conversation-source-job",
|
|
197
|
+
selected_label: selected.label || "",
|
|
198
|
+
selected_selector: selected.selector || "",
|
|
199
|
+
job_options: dedupedFallback
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
selector: lastPrimary.selector,
|
|
205
|
+
source: lastPrimary.source,
|
|
206
|
+
selected_label: selected.label || "",
|
|
207
|
+
selected_selector: selected.selector || "",
|
|
208
|
+
job_options: []
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function matchJobOption(option, jobLabel = "") {
|
|
213
|
+
const requested = normalizeJobText(jobLabel).toLowerCase();
|
|
214
|
+
if (!requested) return false;
|
|
215
|
+
return [
|
|
216
|
+
option.value,
|
|
217
|
+
option.label,
|
|
218
|
+
option.title
|
|
219
|
+
].map((value) => normalizeJobText(value).toLowerCase()).some((value) => (
|
|
220
|
+
value === requested
|
|
221
|
+
|| value.includes(requested)
|
|
222
|
+
|| requested.includes(value)
|
|
223
|
+
));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function clickFirstVisible(client, rootNodeId, selectors = []) {
|
|
227
|
+
for (const selector of selectors) {
|
|
228
|
+
const nodeIds = await safeQuerySelectorAll(client, rootNodeId, selector);
|
|
229
|
+
for (const nodeId of nodeIds) {
|
|
230
|
+
try {
|
|
231
|
+
const box = await getNodeBox(client, nodeId);
|
|
232
|
+
if (box.rect.width <= 2 || box.rect.height <= 2) continue;
|
|
233
|
+
await clickPoint(client, box.center.x, box.center.y);
|
|
234
|
+
return {
|
|
235
|
+
clicked: true,
|
|
236
|
+
selector,
|
|
237
|
+
node_id: nodeId,
|
|
238
|
+
center: box.center
|
|
239
|
+
};
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
clicked: false,
|
|
245
|
+
selector: "",
|
|
246
|
+
node_id: 0
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function selectChatJob(client, rootNodeId, {
|
|
251
|
+
jobLabel = "",
|
|
252
|
+
timeoutMs = 12000,
|
|
253
|
+
intervalMs = 300,
|
|
254
|
+
settleMs = 800
|
|
255
|
+
} = {}) {
|
|
256
|
+
const requested = normalizeJobText(jobLabel);
|
|
257
|
+
if (!requested) {
|
|
258
|
+
return {
|
|
259
|
+
selected: false,
|
|
260
|
+
reason: "missing_job_label"
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let currentRootNodeId = await freshTopRootNodeId(client, rootNodeId);
|
|
265
|
+
let optionsResult = await readChatJobOptions(client, currentRootNodeId, {
|
|
266
|
+
timeoutMs: Math.min(timeoutMs, 1500),
|
|
267
|
+
intervalMs
|
|
268
|
+
});
|
|
269
|
+
let matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
270
|
+
if (!matched || !matched.visible) {
|
|
271
|
+
const triggerRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
272
|
+
const trigger = await clickFirstVisible(client, triggerRootNodeId, CHAT_JOB_TRIGGER_SELECTORS);
|
|
273
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
274
|
+
currentRootNodeId = await freshTopRootNodeId(client, triggerRootNodeId);
|
|
275
|
+
optionsResult = await readChatJobOptions(client, currentRootNodeId, {
|
|
276
|
+
timeoutMs,
|
|
277
|
+
intervalMs
|
|
278
|
+
});
|
|
279
|
+
matched = (optionsResult.job_options || []).find((option) => matchJobOption(option, requested)) || null;
|
|
280
|
+
if (!matched || !matched.visible) {
|
|
281
|
+
return {
|
|
282
|
+
selected: false,
|
|
283
|
+
reason: matched ? "job_option_not_visible" : "job_option_not_found",
|
|
284
|
+
requested,
|
|
285
|
+
trigger,
|
|
286
|
+
options: optionsResult.job_options || [],
|
|
287
|
+
selected_label_before: optionsResult.selected_label || ""
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (matched.active || normalizeJobText(optionsResult.selected_label).toLowerCase() === normalizeJobText(matched.label).toLowerCase()) {
|
|
293
|
+
return {
|
|
294
|
+
selected: true,
|
|
295
|
+
already_current: true,
|
|
296
|
+
requested,
|
|
297
|
+
selected_option: matched,
|
|
298
|
+
options: optionsResult.job_options || [],
|
|
299
|
+
selected_label: optionsResult.selected_label || matched.label
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (matched.center) {
|
|
304
|
+
await clickPoint(client, matched.center.x, matched.center.y);
|
|
305
|
+
} else {
|
|
306
|
+
await clickNodeCenter(client, matched.node_id, {
|
|
307
|
+
scrollIntoView: true
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
311
|
+
|
|
312
|
+
const afterRootNodeId = await freshTopRootNodeId(client, currentRootNodeId);
|
|
313
|
+
const after = await readChatJobOptions(client, afterRootNodeId, {
|
|
314
|
+
timeoutMs: Math.min(timeoutMs, 3000),
|
|
315
|
+
intervalMs
|
|
316
|
+
});
|
|
317
|
+
const afterMatch = (after.job_options || []).find((option) => matchJobOption(option, matched.label)) || matched;
|
|
318
|
+
const selectedLabel = normalizeJobText(after.selected_label || afterMatch.label || "");
|
|
319
|
+
const verified = selectedLabel
|
|
320
|
+
? matchJobOption({ label: selectedLabel, value: selectedLabel, title: selectedLabel }, matched.label)
|
|
321
|
+
: true;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
selected: true,
|
|
325
|
+
verified,
|
|
326
|
+
already_current: false,
|
|
327
|
+
requested,
|
|
328
|
+
selected_option: afterMatch,
|
|
329
|
+
options: after.job_options || optionsResult.job_options || [],
|
|
330
|
+
selected_label: selectedLabel,
|
|
331
|
+
before: optionsResult,
|
|
332
|
+
after
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getMainFrameUrl,
|
|
3
|
+
waitForMainFrameUrl
|
|
4
|
+
} from "../../core/browser/index.js";
|
|
5
|
+
import { CHAT_TARGET_URL } from "./constants.js";
|
|
6
|
+
|
|
7
|
+
export const CHAT_FORBIDDEN_TOP_LEVEL_RESUME_CODE = "CHAT_FORBIDDEN_TOP_LEVEL_RESUME_NAVIGATION";
|
|
8
|
+
|
|
9
|
+
export function isChatShellUrl(url = "") {
|
|
10
|
+
const value = String(url || "");
|
|
11
|
+
return /https?:\/\/[^/]*zhipin\.com\/web\/chat\/index(?:[/?#]|$)/i.test(value)
|
|
12
|
+
|| /https?:\/\/[^/]*zhipin\.com\/web\/chat\/index$/i.test(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isForbiddenChatResumeTopLevelUrl(url = "") {
|
|
16
|
+
return /https?:\/\/[^/]*zhipin\.com\/web\/frame\/c-resume\/?/i.test(String(url || ""));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getChatTopLevelState(client) {
|
|
20
|
+
let url = "";
|
|
21
|
+
let error = null;
|
|
22
|
+
try {
|
|
23
|
+
url = await getMainFrameUrl(client);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
error = err?.message || String(err);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
url,
|
|
29
|
+
is_chat_shell: isChatShellUrl(url),
|
|
30
|
+
is_forbidden_resume_top_level: isForbiddenChatResumeTopLevelUrl(url),
|
|
31
|
+
error
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function makeForbiddenChatResumeNavigationError(pageState, message = "") {
|
|
36
|
+
const error = new Error(message || `Chat tab navigated to forbidden top-level resume URL: ${pageState?.url || "unknown"}`);
|
|
37
|
+
error.code = CHAT_FORBIDDEN_TOP_LEVEL_RESUME_CODE;
|
|
38
|
+
error.page_state = pageState || null;
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isForbiddenChatResumeNavigationError(error) {
|
|
43
|
+
return error?.code === CHAT_FORBIDDEN_TOP_LEVEL_RESUME_CODE
|
|
44
|
+
|| /CHAT_FORBIDDEN_TOP_LEVEL_RESUME_NAVIGATION/i.test(String(error?.message || error || ""));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function assertChatShellNotResumeTopLevel(client, {
|
|
48
|
+
context = "chat"
|
|
49
|
+
} = {}) {
|
|
50
|
+
const state = await getChatTopLevelState(client);
|
|
51
|
+
if (state.is_forbidden_resume_top_level) {
|
|
52
|
+
throw makeForbiddenChatResumeNavigationError(
|
|
53
|
+
state,
|
|
54
|
+
`CHAT_FORBIDDEN_TOP_LEVEL_RESUME_NAVIGATION during ${context}: ${state.url}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function recoverChatShell(client, {
|
|
61
|
+
targetUrl = CHAT_TARGET_URL,
|
|
62
|
+
timeoutMs = 60000,
|
|
63
|
+
intervalMs = 500
|
|
64
|
+
} = {}) {
|
|
65
|
+
const before = await getChatTopLevelState(client);
|
|
66
|
+
if (before.is_chat_shell) {
|
|
67
|
+
return {
|
|
68
|
+
recovered: false,
|
|
69
|
+
before,
|
|
70
|
+
after: before,
|
|
71
|
+
navigate_url: null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await client.Page.navigate({ url: targetUrl });
|
|
76
|
+
const waited = await waitForMainFrameUrl(client, isChatShellUrl, {
|
|
77
|
+
timeoutMs,
|
|
78
|
+
intervalMs
|
|
79
|
+
});
|
|
80
|
+
const after = await getChatTopLevelState(client);
|
|
81
|
+
return {
|
|
82
|
+
recovered: waited.ok && after.is_chat_shell,
|
|
83
|
+
before,
|
|
84
|
+
after,
|
|
85
|
+
wait: waited,
|
|
86
|
+
navigate_url: targetUrl
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDocumentRoot,
|
|
3
|
+
querySelector,
|
|
4
|
+
sleep
|
|
5
|
+
} from "../../core/browser/index.js";
|
|
6
|
+
|
|
7
|
+
export async function getChatRoots(client) {
|
|
8
|
+
const topRoot = await getDocumentRoot(client);
|
|
9
|
+
return {
|
|
10
|
+
topRoot,
|
|
11
|
+
roots: [
|
|
12
|
+
{ name: "top", nodeId: topRoot.nodeId }
|
|
13
|
+
],
|
|
14
|
+
rootNodes: {
|
|
15
|
+
top: topRoot.nodeId
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function waitForChatRoots(client, {
|
|
21
|
+
timeoutMs = 12000,
|
|
22
|
+
intervalMs = 300
|
|
23
|
+
} = {}) {
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
let lastState = null;
|
|
26
|
+
let lastError = null;
|
|
27
|
+
while (Date.now() - started <= timeoutMs) {
|
|
28
|
+
try {
|
|
29
|
+
lastState = await getChatRoots(client);
|
|
30
|
+
if (lastState?.rootNodes?.top) return lastState;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
lastError = error;
|
|
33
|
+
}
|
|
34
|
+
await sleep(intervalMs);
|
|
35
|
+
}
|
|
36
|
+
if (lastError && !lastState) throw lastError;
|
|
37
|
+
return lastState;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function queryFirstAcrossChatRoots(client, roots, selectors) {
|
|
41
|
+
for (const root of roots) {
|
|
42
|
+
if (!root?.nodeId) continue;
|
|
43
|
+
for (const selector of selectors) {
|
|
44
|
+
const nodeId = await querySelector(client, root.nodeId, selector);
|
|
45
|
+
if (nodeId) {
|
|
46
|
+
return {
|
|
47
|
+
root: root.name,
|
|
48
|
+
root_node_id: root.nodeId,
|
|
49
|
+
selector,
|
|
50
|
+
node_id: nodeId
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|