@reconcrap/boss-recommend-mcp 2.0.34 → 2.0.36
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
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
waitForRecommendCardNodeIds
|
|
5
5
|
} from "./cards.js";
|
|
6
6
|
import {
|
|
7
|
-
RECOMMEND_RECENT_NOT_VIEW_LABEL
|
|
7
|
+
RECOMMEND_RECENT_NOT_VIEW_LABEL,
|
|
8
|
+
RECOMMEND_TARGET_URL
|
|
8
9
|
} from "./constants.js";
|
|
9
10
|
import { selectAndConfirmFirstSafeFilter } from "./filters.js";
|
|
10
11
|
import { selectRecommendJob } from "./jobs.js";
|
|
@@ -87,6 +88,105 @@ export function buildRecommendFilterSelectionOptions(filter = {}, {
|
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
function refreshFailureReason(method = "") {
|
|
92
|
+
return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function applyRefreshMethod(client, method, {
|
|
96
|
+
jobLabel = "",
|
|
97
|
+
pageScope = "recommend",
|
|
98
|
+
fallbackPageScope = "recommend",
|
|
99
|
+
filter = {},
|
|
100
|
+
targetUrl = RECOMMEND_TARGET_URL,
|
|
101
|
+
forceRecentNotView = true,
|
|
102
|
+
cardTimeoutMs = 30000,
|
|
103
|
+
reloadSettleMs = 8000
|
|
104
|
+
} = {}) {
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
let currentRootState = null;
|
|
107
|
+
let jobSelection = null;
|
|
108
|
+
let pageScopeResult = null;
|
|
109
|
+
let filterResult = null;
|
|
110
|
+
try {
|
|
111
|
+
if (method === "page_navigate") {
|
|
112
|
+
await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
|
|
113
|
+
} else {
|
|
114
|
+
await client.Page.reload({ ignoreCache: true });
|
|
115
|
+
}
|
|
116
|
+
if (reloadSettleMs > 0) await sleep(reloadSettleMs);
|
|
117
|
+
currentRootState = await waitForRecommendRoots(client, {
|
|
118
|
+
timeoutMs: Math.max(45000, reloadSettleMs * 6),
|
|
119
|
+
intervalMs: 500
|
|
120
|
+
});
|
|
121
|
+
if (!currentRootState?.iframe?.documentNodeId) {
|
|
122
|
+
throw new Error("Recommend iframe was not ready after refresh reload");
|
|
123
|
+
}
|
|
124
|
+
if (jobLabel) {
|
|
125
|
+
jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
|
|
126
|
+
jobLabel,
|
|
127
|
+
settleMs: reloadSettleMs > 10000 ? 12000 : 6000
|
|
128
|
+
});
|
|
129
|
+
if (!jobSelection.selected) {
|
|
130
|
+
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
131
|
+
}
|
|
132
|
+
currentRootState = await getRecommendRoots(client);
|
|
133
|
+
}
|
|
134
|
+
pageScopeResult = await selectRecommendPageScope(
|
|
135
|
+
client,
|
|
136
|
+
currentRootState.iframe.documentNodeId,
|
|
137
|
+
{
|
|
138
|
+
pageScope,
|
|
139
|
+
fallbackScope: fallbackPageScope,
|
|
140
|
+
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
141
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
if (!pageScopeResult.selected) {
|
|
145
|
+
throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
|
|
146
|
+
}
|
|
147
|
+
currentRootState = await getRecommendRoots(client);
|
|
148
|
+
filterResult = await selectAndConfirmFirstSafeFilter(
|
|
149
|
+
client,
|
|
150
|
+
currentRootState.iframe.documentNodeId,
|
|
151
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
152
|
+
);
|
|
153
|
+
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
154
|
+
timeoutMs: cardTimeoutMs,
|
|
155
|
+
intervalMs: 500
|
|
156
|
+
});
|
|
157
|
+
if (!cardNodeIds.length) {
|
|
158
|
+
throw new Error("No recommend candidate cards were found after refresh reload");
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
method,
|
|
163
|
+
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
164
|
+
job_selection: jobSelection,
|
|
165
|
+
page_scope: pageScopeResult,
|
|
166
|
+
filter: filterResult,
|
|
167
|
+
card_count: cardNodeIds.length,
|
|
168
|
+
root_state: currentRootState,
|
|
169
|
+
forced_recent_not_view: forceRecentNotView,
|
|
170
|
+
elapsed_ms: Date.now() - started
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
method,
|
|
176
|
+
reason: refreshFailureReason(method),
|
|
177
|
+
error: error?.message || String(error),
|
|
178
|
+
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
179
|
+
job_selection: jobSelection,
|
|
180
|
+
page_scope: pageScopeResult,
|
|
181
|
+
filter: filterResult,
|
|
182
|
+
card_count: 0,
|
|
183
|
+
root_state: currentRootState,
|
|
184
|
+
forced_recent_not_view: forceRecentNotView,
|
|
185
|
+
elapsed_ms: Date.now() - started
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
90
190
|
export async function refreshRecommendListAtEnd(client, {
|
|
91
191
|
rootState = null,
|
|
92
192
|
jobLabel = "",
|
|
@@ -94,15 +194,18 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
94
194
|
fallbackPageScope = "recommend",
|
|
95
195
|
filter = {},
|
|
96
196
|
preferEndRefreshButton = true,
|
|
197
|
+
forceNavigate = false,
|
|
198
|
+
targetUrl = RECOMMEND_TARGET_URL,
|
|
97
199
|
forceRecentNotView = true,
|
|
98
200
|
cardTimeoutMs = 30000,
|
|
99
201
|
buttonSettleMs = 8000,
|
|
100
202
|
reloadSettleMs = 8000
|
|
101
203
|
} = {}) {
|
|
102
204
|
const attempts = [];
|
|
103
|
-
let currentRootState = rootState ||
|
|
205
|
+
let currentRootState = rootState || null;
|
|
104
206
|
|
|
105
207
|
if (preferEndRefreshButton) {
|
|
208
|
+
currentRootState = currentRootState || await getRecommendRoots(client);
|
|
106
209
|
const buttonResult = await clickRecommendEndRefreshButton(
|
|
107
210
|
client,
|
|
108
211
|
currentRootState.iframe.documentNodeId,
|
|
@@ -156,71 +259,49 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
156
259
|
}
|
|
157
260
|
}
|
|
158
261
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
262
|
+
const fallbackMethods = [];
|
|
263
|
+
if (forceNavigate && typeof client?.Page?.navigate === "function") {
|
|
264
|
+
fallbackMethods.push("page_navigate");
|
|
265
|
+
}
|
|
266
|
+
if (typeof client?.Page?.reload === "function") {
|
|
267
|
+
fallbackMethods.push("page_reload");
|
|
268
|
+
}
|
|
269
|
+
if (!fallbackMethods.length) {
|
|
270
|
+
fallbackMethods.push("page_reload");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let lastRefreshResult = null;
|
|
274
|
+
for (const method of fallbackMethods) {
|
|
275
|
+
const refreshResult = await applyRefreshMethod(client, method, {
|
|
276
|
+
jobLabel,
|
|
277
|
+
pageScope,
|
|
278
|
+
fallbackPageScope,
|
|
279
|
+
filter,
|
|
280
|
+
targetUrl,
|
|
281
|
+
forceRecentNotView,
|
|
282
|
+
cardTimeoutMs,
|
|
283
|
+
reloadSettleMs
|
|
165
284
|
});
|
|
166
|
-
if (
|
|
167
|
-
|
|
285
|
+
if (refreshResult.ok) {
|
|
286
|
+
return {
|
|
287
|
+
...refreshResult,
|
|
288
|
+
attempts
|
|
289
|
+
};
|
|
168
290
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (!jobSelection.selected) {
|
|
176
|
-
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
177
|
-
}
|
|
178
|
-
currentRootState = await getRecommendRoots(client);
|
|
179
|
-
}
|
|
180
|
-
const pageScopeResult = await selectRecommendPageScope(
|
|
181
|
-
client,
|
|
182
|
-
currentRootState.iframe.documentNodeId,
|
|
183
|
-
{
|
|
184
|
-
pageScope,
|
|
185
|
-
fallbackScope: fallbackPageScope,
|
|
186
|
-
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
187
|
-
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
188
|
-
}
|
|
189
|
-
);
|
|
190
|
-
if (!pageScopeResult.selected) {
|
|
191
|
-
throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
|
|
192
|
-
}
|
|
193
|
-
currentRootState = await getRecommendRoots(client);
|
|
194
|
-
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
195
|
-
client,
|
|
196
|
-
currentRootState.iframe.documentNodeId,
|
|
197
|
-
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
198
|
-
);
|
|
199
|
-
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
200
|
-
timeoutMs: cardTimeoutMs,
|
|
201
|
-
intervalMs: 500
|
|
202
|
-
});
|
|
203
|
-
return {
|
|
204
|
-
ok: cardNodeIds.length > 0,
|
|
205
|
-
method: "page_reload",
|
|
206
|
-
attempts,
|
|
207
|
-
job_selection: jobSelection,
|
|
208
|
-
page_scope: pageScopeResult,
|
|
209
|
-
filter: filterResult,
|
|
210
|
-
card_count: cardNodeIds.length,
|
|
211
|
-
root_state: currentRootState,
|
|
212
|
-
forced_recent_not_view: forceRecentNotView
|
|
213
|
-
};
|
|
214
|
-
} catch (error) {
|
|
215
|
-
return {
|
|
291
|
+
attempts.push(refreshResult);
|
|
292
|
+
lastRefreshResult = refreshResult;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
...(lastRefreshResult || {
|
|
216
297
|
ok: false,
|
|
217
|
-
method: "page_reload",
|
|
218
|
-
reason: "
|
|
219
|
-
error:
|
|
220
|
-
attempts,
|
|
298
|
+
method: fallbackMethods[fallbackMethods.length - 1] || "page_reload",
|
|
299
|
+
reason: "refresh_failed",
|
|
300
|
+
error: "Recommend refresh did not run",
|
|
221
301
|
card_count: 0,
|
|
222
302
|
root_state: currentRootState,
|
|
223
303
|
forced_recent_not_view: forceRecentNotView
|
|
224
|
-
}
|
|
225
|
-
|
|
304
|
+
}),
|
|
305
|
+
attempts
|
|
306
|
+
};
|
|
226
307
|
}
|
|
@@ -65,7 +65,8 @@ import {
|
|
|
65
65
|
RECOMMEND_BOTTOM_MARKER_SELECTORS,
|
|
66
66
|
RECOMMEND_CARD_SELECTOR,
|
|
67
67
|
RECOMMEND_END_REFRESH_SELECTOR,
|
|
68
|
-
RECOMMEND_LIST_CONTAINER_SELECTORS
|
|
68
|
+
RECOMMEND_LIST_CONTAINER_SELECTORS,
|
|
69
|
+
RECOMMEND_TARGET_URL
|
|
69
70
|
} from "./constants.js";
|
|
70
71
|
import {
|
|
71
72
|
clickRecommendActionControl,
|
|
@@ -354,23 +355,56 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
354
355
|
return {
|
|
355
356
|
ok: Boolean(refreshAttempt.ok),
|
|
356
357
|
method: refreshAttempt.method || "",
|
|
358
|
+
reason: refreshAttempt.reason || null,
|
|
359
|
+
error: refreshAttempt.error || null,
|
|
357
360
|
forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
|
|
361
|
+
target_url: refreshAttempt.target_url || null,
|
|
358
362
|
card_count: refreshAttempt.card_count || 0,
|
|
363
|
+
elapsed_ms: refreshAttempt.elapsed_ms || 0,
|
|
359
364
|
attempts: (refreshAttempt.attempts || []).map((attempt) => ({
|
|
360
365
|
ok: Boolean(attempt.ok),
|
|
361
366
|
method: attempt.method || "",
|
|
362
367
|
reason: attempt.reason || null,
|
|
368
|
+
error: attempt.error || null,
|
|
363
369
|
label: attempt.label || null,
|
|
364
370
|
before_card_count: attempt.before_card_count || 0,
|
|
365
|
-
after_card_count: attempt.after_card_count || 0
|
|
371
|
+
after_card_count: attempt.after_card_count || 0,
|
|
372
|
+
card_count: attempt.card_count || 0,
|
|
373
|
+
elapsed_ms: attempt.elapsed_ms || 0
|
|
366
374
|
})),
|
|
375
|
+
job_selection: compactJobSelection(refreshAttempt.job_selection),
|
|
367
376
|
page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
|
|
368
377
|
filter: compactFilterResult(refreshAttempt.filter)
|
|
369
378
|
};
|
|
370
379
|
}
|
|
371
380
|
|
|
381
|
+
export function countRecommendResultStatuses(results = [], {
|
|
382
|
+
greetCount = 0
|
|
383
|
+
} = {}) {
|
|
384
|
+
return {
|
|
385
|
+
processed: results.length,
|
|
386
|
+
screened: results.length,
|
|
387
|
+
detail_opened: results.filter((item) => item.detail).length,
|
|
388
|
+
passed: results.filter((item) => item.screening?.passed).length,
|
|
389
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
390
|
+
greet_count: greetCount,
|
|
391
|
+
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
392
|
+
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
393
|
+
detail_open_failed: results.filter((item) => (
|
|
394
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
395
|
+
|| item.error?.code === "DETAIL_OPEN_FAILED"
|
|
396
|
+
)).length,
|
|
397
|
+
transient_recovered: results.filter((item) => (
|
|
398
|
+
item.error?.code === "DETAIL_STALE_NODE"
|
|
399
|
+
|| item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
|
|
400
|
+
|| item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
|
|
401
|
+
|| item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
|
|
402
|
+
)).length
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
372
406
|
function countPassedResults(results = []) {
|
|
373
|
-
return results.
|
|
407
|
+
return countRecommendResultStatuses(results).passed;
|
|
374
408
|
}
|
|
375
409
|
|
|
376
410
|
function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
@@ -381,6 +415,13 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
|
381
415
|
};
|
|
382
416
|
}
|
|
383
417
|
|
|
418
|
+
function createRecommendCloseFailureError(closeResult) {
|
|
419
|
+
const error = new Error(closeResult?.reason || "Recommend detail did not close before recovery");
|
|
420
|
+
error.code = "DETAIL_CLOSE_FAILED";
|
|
421
|
+
error.close_result = closeResult || null;
|
|
422
|
+
return error;
|
|
423
|
+
}
|
|
424
|
+
|
|
384
425
|
export function isRecoverableImageCaptureError(error) {
|
|
385
426
|
const code = String(error?.code || "");
|
|
386
427
|
if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
|
|
@@ -535,7 +576,9 @@ export async function runRecommendWorkflow({
|
|
|
535
576
|
const results = [];
|
|
536
577
|
const refreshAttempts = [];
|
|
537
578
|
let refreshRounds = 0;
|
|
579
|
+
let contextRecoveryAttempts = 0;
|
|
538
580
|
let greetCount = 0;
|
|
581
|
+
const candidateRecoveryCounts = new Map();
|
|
539
582
|
let jobSelection = null;
|
|
540
583
|
let pageScopeSelection = null;
|
|
541
584
|
let filterResult = null;
|
|
@@ -550,6 +593,121 @@ export async function runRecommendWorkflow({
|
|
|
550
593
|
validateViewportPoint: true
|
|
551
594
|
}));
|
|
552
595
|
|
|
596
|
+
function updateRecommendProgress(extra = {}) {
|
|
597
|
+
const counts = countRecommendResultStatuses(results, { greetCount });
|
|
598
|
+
const listSnapshot = compactInfiniteListState(listState);
|
|
599
|
+
runControl.updateProgress({
|
|
600
|
+
card_count: cardNodeIds.length,
|
|
601
|
+
target_count: targetPassCount,
|
|
602
|
+
target_count_semantics: "passed_candidates",
|
|
603
|
+
...counts,
|
|
604
|
+
screening_mode: normalizedScreeningMode,
|
|
605
|
+
unique_seen: listSnapshot.seen_count,
|
|
606
|
+
scroll_count: listSnapshot.scroll_count,
|
|
607
|
+
refresh_rounds: refreshRounds,
|
|
608
|
+
refresh_attempts: refreshAttempts.length,
|
|
609
|
+
context_recoveries: contextRecoveryAttempts,
|
|
610
|
+
list_end_reason: listEndReason || null,
|
|
611
|
+
viewport_checks: viewportGuard.getStats().checks,
|
|
612
|
+
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
613
|
+
...extra
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function checkpointInProgressCandidate({
|
|
618
|
+
index = results.length,
|
|
619
|
+
candidateKey = "",
|
|
620
|
+
cardNodeId = null,
|
|
621
|
+
detailStep = "",
|
|
622
|
+
error = null
|
|
623
|
+
} = {}) {
|
|
624
|
+
runControl.checkpoint({
|
|
625
|
+
in_progress_candidate: {
|
|
626
|
+
index,
|
|
627
|
+
key: candidateKey,
|
|
628
|
+
card_node_id: cardNodeId,
|
|
629
|
+
detail_step: detailStep || null,
|
|
630
|
+
counters: countRecommendResultStatuses(results, { greetCount }),
|
|
631
|
+
error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
|
|
632
|
+
},
|
|
633
|
+
candidate_list: compactInfiniteListState(listState)
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
|
|
638
|
+
forceRecentNotView = true
|
|
639
|
+
} = {}) {
|
|
640
|
+
await runControl.waitIfPaused();
|
|
641
|
+
runControl.throwIfCanceled();
|
|
642
|
+
const started = Date.now();
|
|
643
|
+
runControl.setPhase("recommend:recover-context");
|
|
644
|
+
contextRecoveryAttempts += 1;
|
|
645
|
+
const refreshResult = await refreshRecommendListAtEnd(client, {
|
|
646
|
+
rootState,
|
|
647
|
+
jobLabel,
|
|
648
|
+
pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
|
|
649
|
+
fallbackPageScope: normalizedFallbackPageScope,
|
|
650
|
+
filter: normalizedFilter,
|
|
651
|
+
preferEndRefreshButton: false,
|
|
652
|
+
forceNavigate: true,
|
|
653
|
+
targetUrl: targetUrl || RECOMMEND_TARGET_URL,
|
|
654
|
+
forceRecentNotView,
|
|
655
|
+
cardTimeoutMs,
|
|
656
|
+
buttonSettleMs: refreshButtonSettleMs,
|
|
657
|
+
reloadSettleMs: refreshReloadSettleMs
|
|
658
|
+
});
|
|
659
|
+
const compactRefresh = {
|
|
660
|
+
...compactRefreshAttempt(refreshResult),
|
|
661
|
+
context_recovery: true,
|
|
662
|
+
recovery_reason: reason,
|
|
663
|
+
trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
|
|
664
|
+
elapsed_ms: Date.now() - started
|
|
665
|
+
};
|
|
666
|
+
refreshAttempts.push(compactRefresh);
|
|
667
|
+
runControl.checkpoint({
|
|
668
|
+
context_recovery: {
|
|
669
|
+
attempt: contextRecoveryAttempts,
|
|
670
|
+
reason,
|
|
671
|
+
trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
|
|
672
|
+
refresh: compactRefresh,
|
|
673
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
674
|
+
},
|
|
675
|
+
candidate_list: compactInfiniteListState(listState)
|
|
676
|
+
});
|
|
677
|
+
if (!refreshResult.ok) {
|
|
678
|
+
updateRecommendProgress({
|
|
679
|
+
refresh_method: refreshResult.method || null,
|
|
680
|
+
refresh_forced_recent_not_view: forceRecentNotView,
|
|
681
|
+
recovery_reason: reason
|
|
682
|
+
});
|
|
683
|
+
throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
|
|
684
|
+
}
|
|
685
|
+
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
686
|
+
rootState = await ensureRecommendViewport(rootState, "recover_after");
|
|
687
|
+
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
688
|
+
timeoutMs: cardTimeoutMs,
|
|
689
|
+
intervalMs: 300
|
|
690
|
+
});
|
|
691
|
+
resetInfiniteListForRefreshRound(listState, {
|
|
692
|
+
reason: `context_recovery:${reason}`,
|
|
693
|
+
round: contextRecoveryAttempts,
|
|
694
|
+
method: refreshResult.method,
|
|
695
|
+
metadata: {
|
|
696
|
+
card_count: cardNodeIds.length,
|
|
697
|
+
forced_recent_not_view: forceRecentNotView,
|
|
698
|
+
counters: countRecommendResultStatuses(results, { greetCount })
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
listEndReason = "";
|
|
702
|
+
updateRecommendProgress({
|
|
703
|
+
card_count: cardNodeIds.length,
|
|
704
|
+
refresh_method: refreshResult.method || null,
|
|
705
|
+
refresh_forced_recent_not_view: forceRecentNotView,
|
|
706
|
+
recovery_reason: reason
|
|
707
|
+
});
|
|
708
|
+
return refreshResult;
|
|
709
|
+
}
|
|
710
|
+
|
|
553
711
|
runControl.setPhase("recommend:cleanup");
|
|
554
712
|
await closeRecommendDetail(client, { attemptsLimit: 2 });
|
|
555
713
|
|
|
@@ -630,24 +788,8 @@ export async function runRecommendWorkflow({
|
|
|
630
788
|
throw new Error("No recommend candidate cards found for run service");
|
|
631
789
|
}
|
|
632
790
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
target_count: targetPassCount,
|
|
636
|
-
target_count_semantics: "passed_candidates",
|
|
637
|
-
processed: 0,
|
|
638
|
-
screened: 0,
|
|
639
|
-
detail_opened: 0,
|
|
640
|
-
passed: 0,
|
|
641
|
-
greet_count: 0,
|
|
642
|
-
post_action_clicked: 0,
|
|
643
|
-
screening_mode: normalizedScreeningMode,
|
|
644
|
-
llm_screened: 0,
|
|
645
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
646
|
-
scroll_count: 0,
|
|
647
|
-
refresh_rounds: 0,
|
|
648
|
-
refresh_attempts: 0,
|
|
649
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
650
|
-
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
791
|
+
updateRecommendProgress({
|
|
792
|
+
list_end_reason: null
|
|
651
793
|
});
|
|
652
794
|
|
|
653
795
|
while (countPassedResults(results) < targetPassCount) {
|
|
@@ -722,24 +864,11 @@ export async function runRecommendWorkflow({
|
|
|
722
864
|
refresh_round: refreshRounds,
|
|
723
865
|
refresh: compactRefresh
|
|
724
866
|
});
|
|
725
|
-
|
|
867
|
+
updateRecommendProgress({
|
|
726
868
|
card_count: refreshResult.card_count || cardNodeIds.length,
|
|
727
|
-
target_count: targetPassCount,
|
|
728
|
-
target_count_semantics: "passed_candidates",
|
|
729
|
-
processed: results.length,
|
|
730
|
-
screened: results.length,
|
|
731
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
732
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
733
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
734
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
735
|
-
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
736
|
-
refresh_rounds: refreshRounds,
|
|
737
|
-
refresh_attempts: refreshAttempts.length,
|
|
738
869
|
refresh_method: refreshResult.method || null,
|
|
739
870
|
refresh_forced_recent_not_view: true,
|
|
740
|
-
list_end_reason: listEndReason
|
|
741
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
742
|
-
viewport_recoveries: viewportGuard.getStats().recoveries
|
|
871
|
+
list_end_reason: listEndReason
|
|
743
872
|
});
|
|
744
873
|
if (refreshResult.ok) {
|
|
745
874
|
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
@@ -772,12 +901,16 @@ export async function runRecommendWorkflow({
|
|
|
772
901
|
let screeningCandidate = cardCandidate;
|
|
773
902
|
let detailResult = null;
|
|
774
903
|
let recoverableDetailError = null;
|
|
904
|
+
let detailStep = "not_started";
|
|
775
905
|
if (index < effectiveDetailLimit) {
|
|
776
906
|
try {
|
|
777
907
|
await runControl.waitIfPaused();
|
|
778
908
|
runControl.throwIfCanceled();
|
|
779
909
|
runControl.setPhase("recommend:detail");
|
|
910
|
+
detailStep = "ensure_viewport";
|
|
780
911
|
rootState = await ensureRecommendViewport(rootState, "detail");
|
|
912
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
913
|
+
detailStep = "open_detail";
|
|
781
914
|
networkRecorder.clear();
|
|
782
915
|
const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
|
|
783
916
|
cardNodeId,
|
|
@@ -794,6 +927,7 @@ export async function runRecommendWorkflow({
|
|
|
794
927
|
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
795
928
|
screeningCandidate = cardCandidate;
|
|
796
929
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
930
|
+
detailStep = "wait_network";
|
|
797
931
|
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
798
932
|
waitForRecommendDetailNetworkEvents,
|
|
799
933
|
networkRecorder,
|
|
@@ -807,6 +941,7 @@ export async function runRecommendWorkflow({
|
|
|
807
941
|
if (networkWait?.elapsed_ms != null) {
|
|
808
942
|
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
809
943
|
}
|
|
944
|
+
detailStep = "extract_detail";
|
|
810
945
|
detailResult = await extractRecommendDetailCandidate(client, {
|
|
811
946
|
cardCandidate,
|
|
812
947
|
cardNodeId,
|
|
@@ -830,6 +965,7 @@ export async function runRecommendWorkflow({
|
|
|
830
965
|
waitResult: networkWait
|
|
831
966
|
});
|
|
832
967
|
} else {
|
|
968
|
+
detailStep = "wait_capture_target";
|
|
833
969
|
captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
|
|
834
970
|
domain: "recommend",
|
|
835
971
|
timeoutMs: 6000,
|
|
@@ -846,6 +982,7 @@ export async function runRecommendWorkflow({
|
|
|
846
982
|
extension: "jpg"
|
|
847
983
|
});
|
|
848
984
|
try {
|
|
985
|
+
detailStep = "capture_image";
|
|
849
986
|
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
850
987
|
filePath: imageEvidencePath,
|
|
851
988
|
format: "jpeg",
|
|
@@ -879,6 +1016,17 @@ export async function runRecommendWorkflow({
|
|
|
879
1016
|
source = "image";
|
|
880
1017
|
} catch (error) {
|
|
881
1018
|
if (!isRecoverableImageCaptureError(error)) throw error;
|
|
1019
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1020
|
+
if (recoveryCount < 1) {
|
|
1021
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1022
|
+
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
1023
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1024
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1025
|
+
await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
|
|
1026
|
+
forceRecentNotView: true
|
|
1027
|
+
});
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
882
1030
|
imageEvidence = createRecoverableImageCaptureEvidence(error, {
|
|
883
1031
|
elapsedMs: timings.screenshot_capture_ms,
|
|
884
1032
|
filePath: imageEvidencePath,
|
|
@@ -919,6 +1067,17 @@ export async function runRecommendWorkflow({
|
|
|
919
1067
|
screeningCandidate = detailResult.candidate;
|
|
920
1068
|
} catch (error) {
|
|
921
1069
|
if (!isRecoverableRecommendDetailError(error)) throw error;
|
|
1070
|
+
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1071
|
+
if (recoveryCount < 1) {
|
|
1072
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1073
|
+
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
1074
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1075
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1076
|
+
await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
|
|
1077
|
+
forceRecentNotView: true
|
|
1078
|
+
});
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
922
1081
|
recoverableDetailError = error;
|
|
923
1082
|
detailResult = null;
|
|
924
1083
|
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
@@ -994,6 +1153,21 @@ export async function runRecommendWorkflow({
|
|
|
994
1153
|
}
|
|
995
1154
|
if (detailResult && closeDetail) {
|
|
996
1155
|
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
1156
|
+
if (!detailResult.close_result?.closed) {
|
|
1157
|
+
const closeError = createRecommendCloseFailureError(detailResult.close_result);
|
|
1158
|
+
const recovery = await recoverAndReapplyRecommendContext("detail_close_failed", closeError, {
|
|
1159
|
+
forceRecentNotView: true
|
|
1160
|
+
});
|
|
1161
|
+
detailResult.cv_acquisition = {
|
|
1162
|
+
...(detailResult.cv_acquisition || {}),
|
|
1163
|
+
close_recovery: {
|
|
1164
|
+
ok: Boolean(recovery.ok),
|
|
1165
|
+
method: recovery.method || "",
|
|
1166
|
+
forced_recent_not_view: Boolean(recovery.forced_recent_not_view),
|
|
1167
|
+
card_count: recovery.card_count || 0
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
997
1171
|
}
|
|
998
1172
|
timings.total_ms = Date.now() - candidateStarted;
|
|
999
1173
|
const compactResult = {
|
|
@@ -1024,27 +1198,7 @@ export async function runRecommendWorkflow({
|
|
|
1024
1198
|
}
|
|
1025
1199
|
});
|
|
1026
1200
|
|
|
1027
|
-
|
|
1028
|
-
card_count: cardNodeIds.length,
|
|
1029
|
-
target_count: targetPassCount,
|
|
1030
|
-
target_count_semantics: "passed_candidates",
|
|
1031
|
-
processed: results.length,
|
|
1032
|
-
screened: results.length,
|
|
1033
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
1034
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
1035
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
1036
|
-
greet_count: greetCount,
|
|
1037
|
-
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
1038
|
-
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
1039
|
-
detail_open_failed: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "DETAIL_OPEN_FAILED").length,
|
|
1040
|
-
transient_recovered: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_TIMEOUT" || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT").length,
|
|
1041
|
-
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
1042
|
-
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
1043
|
-
refresh_rounds: refreshRounds,
|
|
1044
|
-
refresh_attempts: refreshAttempts.length,
|
|
1045
|
-
list_end_reason: listEndReason || null,
|
|
1046
|
-
viewport_checks: viewportGuard.getStats().checks,
|
|
1047
|
-
viewport_recoveries: viewportGuard.getStats().recoveries,
|
|
1201
|
+
updateRecommendProgress({
|
|
1048
1202
|
last_candidate_id: screeningCandidate.id || null,
|
|
1049
1203
|
last_candidate_key: candidateKey,
|
|
1050
1204
|
last_score: screening.score
|
|
@@ -1097,16 +1251,8 @@ export async function runRecommendWorkflow({
|
|
|
1097
1251
|
list_end_reason: listEndReason || null,
|
|
1098
1252
|
refresh_rounds: refreshRounds,
|
|
1099
1253
|
refresh_attempts: refreshAttempts,
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
detail_opened: results.filter((item) => item.detail).length,
|
|
1103
|
-
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
1104
|
-
detail_open_failed: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "DETAIL_OPEN_FAILED").length,
|
|
1105
|
-
image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
|
|
1106
|
-
transient_recovered: results.filter((item) => item.error?.code === "DETAIL_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_STALE_NODE" || item.error?.code === "IMAGE_CAPTURE_TIMEOUT" || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT").length,
|
|
1107
|
-
passed: results.filter((item) => item.screening.passed).length,
|
|
1108
|
-
greet_count: greetCount,
|
|
1109
|
-
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
1254
|
+
context_recoveries: contextRecoveryAttempts,
|
|
1255
|
+
...countRecommendResultStatuses(results, { greetCount }),
|
|
1110
1256
|
results
|
|
1111
1257
|
};
|
|
1112
1258
|
}
|
|
@@ -1213,7 +1359,11 @@ export function createRecommendRunService({
|
|
|
1213
1359
|
llm_screened: 0,
|
|
1214
1360
|
passed: 0,
|
|
1215
1361
|
greet_count: 0,
|
|
1216
|
-
post_action_clicked: 0
|
|
1362
|
+
post_action_clicked: 0,
|
|
1363
|
+
image_capture_failed: 0,
|
|
1364
|
+
detail_open_failed: 0,
|
|
1365
|
+
transient_recovered: 0,
|
|
1366
|
+
context_recoveries: 0
|
|
1217
1367
|
},
|
|
1218
1368
|
checkpoint: {},
|
|
1219
1369
|
task: (runControl) => workflow({
|