@reconcrap/boss-recommend-mcp 2.0.4 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/chat-mcp.js +2 -1
- package/src/core/browser/index.js +1 -0
- package/src/core/self-heal/index.js +128 -3
- package/src/core/self-heal/viewport.js +564 -0
- package/src/domains/chat/run-service.js +52 -8
- package/src/domains/recommend/detail.js +189 -6
- package/src/domains/recommend/roots.js +4 -2
- package/src/domains/recommend/run-service.js +51 -7
- package/src/domains/recruit/roots.js +2 -1
- package/src/domains/recruit/run-service.js +34 -3
- package/src/index.js +2 -1
- package/src/recommend-mcp.js +2 -1
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getNextInfiniteListCandidate,
|
|
23
23
|
markInfiniteListCandidateProcessed
|
|
24
24
|
} from "../../core/infinite-list/index.js";
|
|
25
|
+
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
25
26
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
26
27
|
import {
|
|
27
28
|
callScreeningLlm,
|
|
@@ -250,9 +251,13 @@ async function setupChatRunContext(client, {
|
|
|
250
251
|
normalizedStartFrom,
|
|
251
252
|
readyTimeoutMs,
|
|
252
253
|
listSettleMs,
|
|
253
|
-
runControl
|
|
254
|
+
runControl,
|
|
255
|
+
ensureViewport = null
|
|
254
256
|
} = {}) {
|
|
255
|
-
|
|
257
|
+
let rootState = await getChatRoots(client);
|
|
258
|
+
if (ensureViewport) {
|
|
259
|
+
rootState = await ensureViewport(rootState, "context_roots");
|
|
260
|
+
}
|
|
256
261
|
runControl.checkpoint({
|
|
257
262
|
top_document_node_id: rootState.rootNodes.top
|
|
258
263
|
});
|
|
@@ -280,6 +285,10 @@ async function setupChatRunContext(client, {
|
|
|
280
285
|
if (normalizeText(job) && !jobSelection.selected) {
|
|
281
286
|
throw new Error(`Chat job selection failed: ${jobSelection.reason || "unknown"}`);
|
|
282
287
|
}
|
|
288
|
+
rootState = await getChatRoots(client);
|
|
289
|
+
if (ensureViewport) {
|
|
290
|
+
rootState = await ensureViewport(rootState, "context_job");
|
|
291
|
+
}
|
|
283
292
|
runControl.checkpoint({
|
|
284
293
|
chat_context_step: "job_selection",
|
|
285
294
|
primary_label: primaryLabel,
|
|
@@ -294,6 +303,10 @@ async function setupChatRunContext(client, {
|
|
|
294
303
|
if (!startFilter.ok) {
|
|
295
304
|
throw new Error(`Chat start filter selection failed: ${startFilter.error || "unknown"}`);
|
|
296
305
|
}
|
|
306
|
+
rootState = await getChatRoots(client);
|
|
307
|
+
if (ensureViewport) {
|
|
308
|
+
rootState = await ensureViewport(rootState, "context_start_filter");
|
|
309
|
+
}
|
|
297
310
|
runControl.checkpoint({
|
|
298
311
|
chat_context_step: "start_filter",
|
|
299
312
|
primary_label: primaryLabel,
|
|
@@ -362,6 +375,18 @@ export async function runChatWorkflow({
|
|
|
362
375
|
domain: "chat",
|
|
363
376
|
listName: "chat-candidates"
|
|
364
377
|
});
|
|
378
|
+
const viewportGuard = createViewportRunGuard({
|
|
379
|
+
client,
|
|
380
|
+
domain: "chat",
|
|
381
|
+
root: "top",
|
|
382
|
+
frameOwnerRoot: "top",
|
|
383
|
+
runControl,
|
|
384
|
+
getRoots: getChatRoots
|
|
385
|
+
});
|
|
386
|
+
async function ensureChatViewport(rootState, phase) {
|
|
387
|
+
const result = await viewportGuard.ensure(rootState, { phase });
|
|
388
|
+
return result.rootState || rootState;
|
|
389
|
+
}
|
|
365
390
|
const results = [];
|
|
366
391
|
let cardNodeIds = [];
|
|
367
392
|
let listEndReason = "";
|
|
@@ -398,7 +423,8 @@ export async function runChatWorkflow({
|
|
|
398
423
|
normalizedStartFrom,
|
|
399
424
|
readyTimeoutMs,
|
|
400
425
|
listSettleMs,
|
|
401
|
-
runControl
|
|
426
|
+
runControl,
|
|
427
|
+
ensureViewport: ensureChatViewport
|
|
402
428
|
});
|
|
403
429
|
let rootState = setup.rootState;
|
|
404
430
|
contextSetup = {
|
|
@@ -431,7 +457,8 @@ export async function runChatWorkflow({
|
|
|
431
457
|
normalizedStartFrom,
|
|
432
458
|
readyTimeoutMs,
|
|
433
459
|
listSettleMs,
|
|
434
|
-
runControl
|
|
460
|
+
runControl,
|
|
461
|
+
ensureViewport: ensureChatViewport
|
|
435
462
|
});
|
|
436
463
|
rootState = recoveredSetup.rootState;
|
|
437
464
|
contextSetup = {
|
|
@@ -449,7 +476,7 @@ export async function runChatWorkflow({
|
|
|
449
476
|
await runControl.waitIfPaused();
|
|
450
477
|
runControl.throwIfCanceled();
|
|
451
478
|
runControl.setPhase("chat:cards");
|
|
452
|
-
const cardRootState = await getChatRoots(client);
|
|
479
|
+
const cardRootState = await ensureChatViewport(await getChatRoots(client), "cards");
|
|
453
480
|
const initialCards = await waitForChatCandidateNodeIds(client, cardRootState.rootNodes.top, {
|
|
454
481
|
timeoutMs: cardTimeoutMs,
|
|
455
482
|
intervalMs: 500
|
|
@@ -479,7 +506,9 @@ export async function runChatWorkflow({
|
|
|
479
506
|
request_skipped: 0,
|
|
480
507
|
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
481
508
|
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
482
|
-
list_end_reason: listEndReason
|
|
509
|
+
list_end_reason: listEndReason,
|
|
510
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
511
|
+
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
483
512
|
});
|
|
484
513
|
runControl.setPhase("chat:done");
|
|
485
514
|
return {
|
|
@@ -493,6 +522,10 @@ export async function runChatWorkflow({
|
|
|
493
522
|
requested_start_from: normalizedStartFrom
|
|
494
523
|
},
|
|
495
524
|
candidate_list: compactInfiniteListState(listState),
|
|
525
|
+
viewport_health: {
|
|
526
|
+
stats: viewportGuard.getStats(),
|
|
527
|
+
events: viewportGuard.getEvents()
|
|
528
|
+
},
|
|
496
529
|
list_end_reason: listEndReason,
|
|
497
530
|
target_pass_count: passTarget,
|
|
498
531
|
process_until_list_end: Boolean(processUntilListEnd),
|
|
@@ -524,7 +557,9 @@ export async function runChatWorkflow({
|
|
|
524
557
|
request_satisfied: 0,
|
|
525
558
|
request_skipped: 0,
|
|
526
559
|
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
527
|
-
scroll_count: 0
|
|
560
|
+
scroll_count: 0,
|
|
561
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
562
|
+
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
528
563
|
});
|
|
529
564
|
|
|
530
565
|
while (
|
|
@@ -537,6 +572,7 @@ export async function runChatWorkflow({
|
|
|
537
572
|
await runControl.waitIfPaused();
|
|
538
573
|
runControl.throwIfCanceled();
|
|
539
574
|
runControl.setPhase("chat:candidate");
|
|
575
|
+
rootState = await ensureChatViewport(rootState, "candidate_loop");
|
|
540
576
|
const loopTopLevelState = await getChatTopLevelState(client);
|
|
541
577
|
if (!loopTopLevelState.is_chat_shell) {
|
|
542
578
|
await recoverAndReapplyChatContext("candidate_loop_non_chat_shell", {
|
|
@@ -554,7 +590,8 @@ export async function runChatWorkflow({
|
|
|
554
590
|
settleMs: listSettleMs,
|
|
555
591
|
fallbackPoint: listFallbackPoint,
|
|
556
592
|
findNodeIds: async () => {
|
|
557
|
-
const currentRootState = await getChatRoots(client);
|
|
593
|
+
const currentRootState = await ensureChatViewport(await getChatRoots(client), "candidate_find_nodes");
|
|
594
|
+
rootState = currentRootState;
|
|
558
595
|
const currentCards = await waitForChatCandidateNodeIds(client, currentRootState.rootNodes.top, {
|
|
559
596
|
timeoutMs: Math.min(cardTimeoutMs, 8000),
|
|
560
597
|
intervalMs: 500
|
|
@@ -609,6 +646,7 @@ export async function runChatWorkflow({
|
|
|
609
646
|
await runControl.waitIfPaused();
|
|
610
647
|
runControl.throwIfCanceled();
|
|
611
648
|
runControl.setPhase("chat:detail");
|
|
649
|
+
rootState = await ensureChatViewport(rootState, "detail");
|
|
612
650
|
|
|
613
651
|
detailStep = "select_candidate";
|
|
614
652
|
networkRecorder.clear();
|
|
@@ -921,6 +959,8 @@ export async function runChatWorkflow({
|
|
|
921
959
|
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
922
960
|
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
923
961
|
list_end_reason: listEndReason || null,
|
|
962
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
963
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
924
964
|
last_candidate_id: screeningCandidate.id || null,
|
|
925
965
|
last_candidate_key: candidateKey,
|
|
926
966
|
last_score: screening.score
|
|
@@ -950,6 +990,10 @@ export async function runChatWorkflow({
|
|
|
950
990
|
card_count: cardNodeIds.length,
|
|
951
991
|
context_setup: contextSetup,
|
|
952
992
|
candidate_list: compactInfiniteListState(listState),
|
|
993
|
+
viewport_health: {
|
|
994
|
+
stats: viewportGuard.getStats(),
|
|
995
|
+
events: viewportGuard.getEvents()
|
|
996
|
+
},
|
|
953
997
|
list_end_reason: listEndReason || null,
|
|
954
998
|
target_pass_count: passTarget,
|
|
955
999
|
process_until_list_end: Boolean(processUntilListEnd),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
querySelectorAll,
|
|
9
9
|
sleep
|
|
10
10
|
} from "../../core/browser/index.js";
|
|
11
|
+
import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
|
|
11
12
|
import {
|
|
12
13
|
buildScreeningCandidateFromDetail,
|
|
13
14
|
htmlToText
|
|
@@ -22,6 +23,10 @@ import {
|
|
|
22
23
|
getRecommendRoots,
|
|
23
24
|
queryFirstAcrossRoots
|
|
24
25
|
} from "./roots.js";
|
|
26
|
+
import {
|
|
27
|
+
findRecommendCardNodeIds,
|
|
28
|
+
readRecommendCardCandidate
|
|
29
|
+
} from "./cards.js";
|
|
25
30
|
|
|
26
31
|
export function matchesRecommendDetailNetwork(url) {
|
|
27
32
|
return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
@@ -146,14 +151,36 @@ export async function readRecommendDetailHtml(client, detailState) {
|
|
|
146
151
|
let popupHTML = "";
|
|
147
152
|
let resumeHTML = "";
|
|
148
153
|
let resumeIframeDocumentNodeId = null;
|
|
154
|
+
const errors = [];
|
|
149
155
|
|
|
150
156
|
if (detailState?.popup?.node_id) {
|
|
151
|
-
|
|
157
|
+
try {
|
|
158
|
+
popupHTML = await getOuterHTML(client, detailState.popup.node_id);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
errors.push({
|
|
161
|
+
source: "popup",
|
|
162
|
+
node_id: detailState.popup.node_id,
|
|
163
|
+
stale_node: isStaleRecommendNodeError(error),
|
|
164
|
+
error: error?.message || String(error)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
if (detailState?.resumeIframe?.node_id) {
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
try {
|
|
171
|
+
resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
|
|
172
|
+
resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
errors.push({
|
|
175
|
+
source: "resume_iframe",
|
|
176
|
+
node_id: detailState.resumeIframe.node_id,
|
|
177
|
+
document_node_id: resumeIframeDocumentNodeId,
|
|
178
|
+
stale_node: isStaleRecommendNodeError(error),
|
|
179
|
+
error: error?.message || String(error)
|
|
180
|
+
});
|
|
181
|
+
resumeIframeDocumentNodeId = null;
|
|
182
|
+
resumeHTML = "";
|
|
183
|
+
}
|
|
157
184
|
}
|
|
158
185
|
|
|
159
186
|
return {
|
|
@@ -161,7 +188,90 @@ export async function readRecommendDetailHtml(client, detailState) {
|
|
|
161
188
|
resumeHTML,
|
|
162
189
|
resumeIframeDocumentNodeId,
|
|
163
190
|
popupText: htmlToText(popupHTML),
|
|
164
|
-
resumeText: htmlToText(resumeHTML)
|
|
191
|
+
resumeText: htmlToText(resumeHTML),
|
|
192
|
+
errors
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isStaleRecommendNodeError(error) {
|
|
197
|
+
const message = String(error?.message || error || "");
|
|
198
|
+
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function findRecommendCardNodeForCandidateKey(client, {
|
|
202
|
+
candidateKey = "",
|
|
203
|
+
rootState = null,
|
|
204
|
+
targetUrl = "",
|
|
205
|
+
source = "recommend-run-card-retry",
|
|
206
|
+
timeoutMs = 5000,
|
|
207
|
+
intervalMs = 250
|
|
208
|
+
} = {}) {
|
|
209
|
+
if (!candidateKey) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
reason: "candidate_key_required"
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const started = Date.now();
|
|
217
|
+
let lastError = null;
|
|
218
|
+
let lastCardCount = 0;
|
|
219
|
+
while (Date.now() - started <= timeoutMs) {
|
|
220
|
+
const currentRootState = rootState?.iframe?.documentNodeId
|
|
221
|
+
? rootState
|
|
222
|
+
: await getRecommendRoots(client);
|
|
223
|
+
const frameNodeId = currentRootState?.iframe?.documentNodeId;
|
|
224
|
+
if (!frameNodeId) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
reason: "recommend_frame_not_found"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
|
|
232
|
+
lastCardCount = nodeIds.length;
|
|
233
|
+
for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
|
|
234
|
+
const nodeId = nodeIds[visibleIndex];
|
|
235
|
+
try {
|
|
236
|
+
const candidate = await readRecommendCardCandidate(client, nodeId, {
|
|
237
|
+
targetUrl,
|
|
238
|
+
source,
|
|
239
|
+
metadata: {
|
|
240
|
+
visible_index: visibleIndex,
|
|
241
|
+
retry_reason: "stale_detail_node"
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
const key = candidateKeyFromProfile(candidate, {
|
|
245
|
+
nodeId,
|
|
246
|
+
visibleIndex,
|
|
247
|
+
attributes: candidate?.attributes || candidate?.metadata?.attributes || {}
|
|
248
|
+
});
|
|
249
|
+
if (key === candidateKey) {
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
node_id: nodeId,
|
|
253
|
+
visible_index: visibleIndex,
|
|
254
|
+
candidate,
|
|
255
|
+
key,
|
|
256
|
+
root_state: currentRootState,
|
|
257
|
+
card_count: nodeIds.length
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
lastError = error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (intervalMs > 0) await sleep(intervalMs);
|
|
266
|
+
rootState = null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
reason: "candidate_key_not_mounted",
|
|
272
|
+
candidate_key: candidateKey,
|
|
273
|
+
last_card_count: lastCardCount,
|
|
274
|
+
error: lastError?.message || null
|
|
165
275
|
};
|
|
166
276
|
}
|
|
167
277
|
|
|
@@ -181,6 +291,77 @@ export async function openRecommendCardDetail(client, cardNodeId, {
|
|
|
181
291
|
};
|
|
182
292
|
}
|
|
183
293
|
|
|
294
|
+
export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
295
|
+
cardNodeId,
|
|
296
|
+
candidateKey = "",
|
|
297
|
+
cardCandidate = null,
|
|
298
|
+
rootState = null,
|
|
299
|
+
targetUrl = "",
|
|
300
|
+
timeoutMs = 12000,
|
|
301
|
+
scrollIntoView = true,
|
|
302
|
+
retryTimeoutMs = 5000,
|
|
303
|
+
retryIntervalMs = 250,
|
|
304
|
+
maxAttempts = 2
|
|
305
|
+
} = {}) {
|
|
306
|
+
let currentNodeId = cardNodeId;
|
|
307
|
+
let currentCandidate = cardCandidate;
|
|
308
|
+
let currentRootState = rootState;
|
|
309
|
+
const attempts = [];
|
|
310
|
+
const limit = Math.max(1, Number(maxAttempts) || 1);
|
|
311
|
+
|
|
312
|
+
for (let attemptIndex = 0; attemptIndex < limit; attemptIndex += 1) {
|
|
313
|
+
try {
|
|
314
|
+
const opened = await openRecommendCardDetail(client, currentNodeId, {
|
|
315
|
+
timeoutMs,
|
|
316
|
+
scrollIntoView
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
...opened,
|
|
320
|
+
card_node_id: currentNodeId,
|
|
321
|
+
card_candidate: currentCandidate,
|
|
322
|
+
retry_attempts: attempts
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const stale = isStaleRecommendNodeError(error);
|
|
326
|
+
attempts.push({
|
|
327
|
+
attempt: attemptIndex + 1,
|
|
328
|
+
node_id: currentNodeId,
|
|
329
|
+
stale_node: stale,
|
|
330
|
+
error: error?.message || String(error)
|
|
331
|
+
});
|
|
332
|
+
if (!stale || attemptIndex >= limit - 1 || !candidateKey) {
|
|
333
|
+
error.recommend_detail_open_attempts = attempts;
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const resolved = await findRecommendCardNodeForCandidateKey(client, {
|
|
338
|
+
candidateKey,
|
|
339
|
+
rootState: currentRootState,
|
|
340
|
+
targetUrl,
|
|
341
|
+
timeoutMs: retryTimeoutMs,
|
|
342
|
+
intervalMs: retryIntervalMs
|
|
343
|
+
});
|
|
344
|
+
attempts[attempts.length - 1].refresh_lookup = {
|
|
345
|
+
ok: Boolean(resolved.ok),
|
|
346
|
+
node_id: resolved.node_id || null,
|
|
347
|
+
visible_index: resolved.visible_index ?? null,
|
|
348
|
+
card_count: resolved.card_count || resolved.last_card_count || 0,
|
|
349
|
+
reason: resolved.reason || null,
|
|
350
|
+
error: resolved.error || null
|
|
351
|
+
};
|
|
352
|
+
if (!resolved.ok || !resolved.node_id) {
|
|
353
|
+
error.recommend_detail_open_attempts = attempts;
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
currentNodeId = resolved.node_id;
|
|
357
|
+
currentCandidate = resolved.candidate || currentCandidate;
|
|
358
|
+
currentRootState = resolved.root_state || null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
throw new Error("Recommend detail retry exhausted");
|
|
363
|
+
}
|
|
364
|
+
|
|
184
365
|
export async function closeRecommendDetail(client, {
|
|
185
366
|
attemptsLimit = 3
|
|
186
367
|
} = {}) {
|
|
@@ -317,7 +498,8 @@ export async function extractRecommendDetailCandidate(client, {
|
|
|
317
498
|
detail_popup_root: detailState?.popup?.root || null,
|
|
318
499
|
resume_iframe_selector: detailState?.resumeIframe?.selector || null,
|
|
319
500
|
resume_iframe_root: detailState?.resumeIframe?.root || null,
|
|
320
|
-
resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
|
|
501
|
+
resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
|
|
502
|
+
detail_html_errors: detailHtml.errors || []
|
|
321
503
|
}
|
|
322
504
|
});
|
|
323
505
|
|
|
@@ -334,7 +516,8 @@ export async function extractRecommendDetailCandidate(client, {
|
|
|
334
516
|
popup_text: detailHtml.popupText,
|
|
335
517
|
resume_text: detailHtml.resumeText,
|
|
336
518
|
popup_html_length: detailHtml.popupHTML.length,
|
|
337
|
-
resume_html_length: detailHtml.resumeHTML.length
|
|
519
|
+
resume_html_length: detailHtml.resumeHTML.length,
|
|
520
|
+
html_errors: detailHtml.errors || []
|
|
338
521
|
},
|
|
339
522
|
close_result: closeResult
|
|
340
523
|
};
|
|
@@ -25,7 +25,8 @@ export async function getRecommendRoots(client, {
|
|
|
25
25
|
].filter(Boolean),
|
|
26
26
|
rootNodes: {
|
|
27
27
|
top: topRoot.nodeId,
|
|
28
|
-
frame: iframe?.documentNodeId || 0
|
|
28
|
+
frame: iframe?.documentNodeId || 0,
|
|
29
|
+
frameOwner: iframe?.nodeId || 0
|
|
29
30
|
}
|
|
30
31
|
};
|
|
31
32
|
}
|
|
@@ -49,7 +50,8 @@ export async function waitForRecommendRoots(client, {
|
|
|
49
50
|
roots: [],
|
|
50
51
|
rootNodes: {
|
|
51
52
|
top: 0,
|
|
52
|
-
frame: 0
|
|
53
|
+
frame: 0,
|
|
54
|
+
frameOwner: 0
|
|
53
55
|
}
|
|
54
56
|
};
|
|
55
57
|
}
|
|
@@ -20,12 +20,13 @@ import {
|
|
|
20
20
|
markInfiniteListCandidateProcessed,
|
|
21
21
|
resetInfiniteListForRefreshRound
|
|
22
22
|
} from "../../core/infinite-list/index.js";
|
|
23
|
+
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
23
24
|
import { screenCandidate } from "../../core/screening/index.js";
|
|
24
25
|
import {
|
|
25
26
|
closeRecommendDetail,
|
|
26
27
|
createRecommendDetailNetworkRecorder,
|
|
27
28
|
extractRecommendDetailCandidate,
|
|
28
|
-
|
|
29
|
+
openRecommendCardDetailWithFreshRetry,
|
|
29
30
|
waitForRecommendDetailNetworkEvents
|
|
30
31
|
} from "./detail.js";
|
|
31
32
|
import {
|
|
@@ -372,6 +373,18 @@ export async function runRecommendWorkflow({
|
|
|
372
373
|
domain: "recommend",
|
|
373
374
|
listName: "recommend-candidates"
|
|
374
375
|
});
|
|
376
|
+
const viewportGuard = createViewportRunGuard({
|
|
377
|
+
client,
|
|
378
|
+
domain: "recommend",
|
|
379
|
+
root: "frame",
|
|
380
|
+
frameOwnerRoot: "frameOwner",
|
|
381
|
+
runControl,
|
|
382
|
+
getRoots: getRecommendRoots
|
|
383
|
+
});
|
|
384
|
+
async function ensureRecommendViewport(rootState, phase) {
|
|
385
|
+
const result = await viewportGuard.ensure(rootState, { phase });
|
|
386
|
+
return result.rootState || rootState;
|
|
387
|
+
}
|
|
375
388
|
const results = [];
|
|
376
389
|
const refreshAttempts = [];
|
|
377
390
|
let refreshRounds = 0;
|
|
@@ -389,6 +402,7 @@ export async function runRecommendWorkflow({
|
|
|
389
402
|
runControl.throwIfCanceled();
|
|
390
403
|
runControl.setPhase("recommend:roots");
|
|
391
404
|
let rootState = await getRecommendRoots(client);
|
|
405
|
+
rootState = await ensureRecommendViewport(rootState, "roots");
|
|
392
406
|
runControl.checkpoint({
|
|
393
407
|
iframe_selector: rootState.iframe.selector,
|
|
394
408
|
iframe_document_node_id: rootState.iframe.documentNodeId
|
|
@@ -406,6 +420,7 @@ export async function runRecommendWorkflow({
|
|
|
406
420
|
throw new Error(`Requested recommend job was not selected: ${jobSelection.reason}`);
|
|
407
421
|
}
|
|
408
422
|
rootState = await getRecommendRoots(client);
|
|
423
|
+
rootState = await ensureRecommendViewport(rootState, "job");
|
|
409
424
|
runControl.checkpoint({
|
|
410
425
|
job_selection: compactJobSelection(jobSelection)
|
|
411
426
|
});
|
|
@@ -424,6 +439,7 @@ export async function runRecommendWorkflow({
|
|
|
424
439
|
throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
|
|
425
440
|
}
|
|
426
441
|
rootState = await getRecommendRoots(client);
|
|
442
|
+
rootState = await ensureRecommendViewport(rootState, "page_scope");
|
|
427
443
|
runControl.checkpoint({
|
|
428
444
|
page_scope: compactPageScopeSelection(pageScopeSelection)
|
|
429
445
|
});
|
|
@@ -440,6 +456,8 @@ export async function runRecommendWorkflow({
|
|
|
440
456
|
if (!filterResult.confirmed) {
|
|
441
457
|
throw new Error("Recommend run filter selection was not confirmed");
|
|
442
458
|
}
|
|
459
|
+
rootState = await getRecommendRoots(client);
|
|
460
|
+
rootState = await ensureRecommendViewport(rootState, "filter");
|
|
443
461
|
runControl.checkpoint({
|
|
444
462
|
filter: compactFilterResult(filterResult)
|
|
445
463
|
});
|
|
@@ -448,6 +466,7 @@ export async function runRecommendWorkflow({
|
|
|
448
466
|
await runControl.waitIfPaused();
|
|
449
467
|
runControl.throwIfCanceled();
|
|
450
468
|
runControl.setPhase("recommend:cards");
|
|
469
|
+
rootState = await ensureRecommendViewport(rootState, "cards");
|
|
451
470
|
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
452
471
|
timeoutMs: cardTimeoutMs,
|
|
453
472
|
intervalMs: 300
|
|
@@ -468,13 +487,16 @@ export async function runRecommendWorkflow({
|
|
|
468
487
|
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
469
488
|
scroll_count: 0,
|
|
470
489
|
refresh_rounds: 0,
|
|
471
|
-
refresh_attempts: 0
|
|
490
|
+
refresh_attempts: 0,
|
|
491
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
492
|
+
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
472
493
|
});
|
|
473
494
|
|
|
474
495
|
while (results.length < limit) {
|
|
475
496
|
await runControl.waitIfPaused();
|
|
476
497
|
runControl.throwIfCanceled();
|
|
477
498
|
runControl.setPhase("recommend:candidate");
|
|
499
|
+
rootState = await ensureRecommendViewport(rootState, "candidate_loop");
|
|
478
500
|
|
|
479
501
|
const nextCandidateResult = await getNextInfiniteListCandidate({
|
|
480
502
|
client,
|
|
@@ -485,7 +507,9 @@ export async function runRecommendWorkflow({
|
|
|
485
507
|
settleMs: listSettleMs,
|
|
486
508
|
fallbackPoint: listFallbackPoint,
|
|
487
509
|
findNodeIds: async () => {
|
|
488
|
-
|
|
510
|
+
let currentRootState = await getRecommendRoots(client);
|
|
511
|
+
currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
|
|
512
|
+
rootState = currentRootState;
|
|
489
513
|
const currentCardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
490
514
|
timeoutMs: Math.min(cardTimeoutMs, 5000),
|
|
491
515
|
intervalMs: 300
|
|
@@ -544,10 +568,13 @@ export async function runRecommendWorkflow({
|
|
|
544
568
|
refresh_attempts: refreshAttempts.length,
|
|
545
569
|
refresh_method: refreshResult.method || null,
|
|
546
570
|
refresh_forced_recent_not_view: true,
|
|
547
|
-
list_end_reason: listEndReason
|
|
571
|
+
list_end_reason: listEndReason,
|
|
572
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
573
|
+
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
548
574
|
});
|
|
549
575
|
if (refreshResult.ok) {
|
|
550
576
|
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
577
|
+
rootState = await ensureRecommendViewport(rootState, "refresh_after");
|
|
551
578
|
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
552
579
|
timeoutMs: cardTimeoutMs,
|
|
553
580
|
intervalMs: 300
|
|
@@ -569,9 +596,9 @@ export async function runRecommendWorkflow({
|
|
|
569
596
|
}
|
|
570
597
|
|
|
571
598
|
const index = results.length;
|
|
572
|
-
|
|
599
|
+
let cardNodeId = nextCandidateResult.item.node_id;
|
|
573
600
|
const candidateKey = nextCandidateResult.item.key;
|
|
574
|
-
|
|
601
|
+
let cardCandidate = nextCandidateResult.item.candidate;
|
|
575
602
|
|
|
576
603
|
let screeningCandidate = cardCandidate;
|
|
577
604
|
let detailResult = null;
|
|
@@ -579,8 +606,19 @@ export async function runRecommendWorkflow({
|
|
|
579
606
|
await runControl.waitIfPaused();
|
|
580
607
|
runControl.throwIfCanceled();
|
|
581
608
|
runControl.setPhase("recommend:detail");
|
|
609
|
+
rootState = await ensureRecommendViewport(rootState, "detail");
|
|
582
610
|
networkRecorder.clear();
|
|
583
|
-
const openedDetail = await
|
|
611
|
+
const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
|
|
612
|
+
cardNodeId,
|
|
613
|
+
candidateKey,
|
|
614
|
+
cardCandidate,
|
|
615
|
+
rootState,
|
|
616
|
+
targetUrl,
|
|
617
|
+
maxAttempts: 2
|
|
618
|
+
});
|
|
619
|
+
cardNodeId = openedDetail.card_node_id || cardNodeId;
|
|
620
|
+
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
621
|
+
screeningCandidate = cardCandidate;
|
|
584
622
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
585
623
|
const networkWait = await waitForCvNetworkEvents(
|
|
586
624
|
waitForRecommendDetailNetworkEvents,
|
|
@@ -719,6 +757,8 @@ export async function runRecommendWorkflow({
|
|
|
719
757
|
refresh_rounds: refreshRounds,
|
|
720
758
|
refresh_attempts: refreshAttempts.length,
|
|
721
759
|
list_end_reason: listEndReason || null,
|
|
760
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
761
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
722
762
|
last_candidate_id: screeningCandidate.id || null,
|
|
723
763
|
last_candidate_key: candidateKey,
|
|
724
764
|
last_score: screening.score
|
|
@@ -756,6 +796,10 @@ export async function runRecommendWorkflow({
|
|
|
756
796
|
filter: compactFilterResult(filterResult),
|
|
757
797
|
card_count: cardNodeIds.length,
|
|
758
798
|
candidate_list: compactInfiniteListState(listState),
|
|
799
|
+
viewport_health: {
|
|
800
|
+
stats: viewportGuard.getStats(),
|
|
801
|
+
events: viewportGuard.getEvents()
|
|
802
|
+
},
|
|
759
803
|
list_end_reason: listEndReason || null,
|
|
760
804
|
refresh_rounds: refreshRounds,
|
|
761
805
|
refresh_attempts: refreshAttempts,
|