@reconcrap/boss-recommend-mcp 2.0.53 → 2.0.55

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.
@@ -1,13 +1,13 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { createRunLifecycleManager } from "../../core/run/index.js";
4
- import {
5
- addTiming,
6
- imageEvidenceFilePath,
7
- measureTiming
8
- } from "../../core/run/timing.js";
9
- import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
- import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRunLifecycleManager } from "../../core/run/index.js";
4
+ import {
5
+ addTiming,
6
+ imageEvidenceFilePath,
7
+ measureTiming
8
+ } from "../../core/run/timing.js";
9
+ import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
+ import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
11
11
  import {
12
12
  configureHumanInteraction,
13
13
  createHumanRestController,
@@ -15,37 +15,38 @@ import {
15
15
  normalizeHumanBehaviorOptions,
16
16
  sleep
17
17
  } from "../../core/browser/index.js";
18
- import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
19
- import {
20
- compactCvAcquisitionState,
21
- countParsedNetworkProfiles,
22
- createCvAcquisitionState,
23
- DEFAULT_MAX_IMAGE_PAGES,
24
- getCvNetworkWaitPlan,
25
- recordCvImageFallback,
26
- recordCvNetworkHit,
27
- recordCvNetworkMiss,
28
- summarizeImageEvidence,
29
- waitForCvNetworkEvents
30
- } from "../../core/cv-acquisition/index.js";
31
- import {
32
- compactInfiniteListState,
33
- createInfiniteListState,
34
- detectInfiniteListBottomMarker,
35
- getNextInfiniteListCandidate,
36
- markInfiniteListCandidateProcessed,
37
- resetInfiniteListForRefreshRound,
38
- resolveInfiniteListFallbackPoint
39
- } from "../../core/infinite-list/index.js";
40
- import { createViewportRunGuard } from "../../core/self-heal/index.js";
41
- import {
42
- callScreeningLlm,
43
- compactScreeningLlmResult,
44
- createFailedLlmScreeningResult,
45
- llmResultToScreening,
46
- screenCandidate
47
- } from "../../core/screening/index.js";
18
+ import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
19
+ import {
20
+ compactCvAcquisitionState,
21
+ countParsedNetworkProfiles,
22
+ createCvAcquisitionState,
23
+ DEFAULT_MAX_IMAGE_PAGES,
24
+ getCvNetworkWaitPlan,
25
+ recordCvImageFallback,
26
+ recordCvNetworkHit,
27
+ recordCvNetworkMiss,
28
+ summarizeImageEvidence,
29
+ waitForCvNetworkEvents
30
+ } from "../../core/cv-acquisition/index.js";
31
+ import {
32
+ compactInfiniteListState,
33
+ createInfiniteListState,
34
+ detectInfiniteListBottomMarker,
35
+ getNextInfiniteListCandidate,
36
+ markInfiniteListCandidateProcessed,
37
+ resetInfiniteListForRefreshRound,
38
+ resolveInfiniteListFallbackPoint
39
+ } from "../../core/infinite-list/index.js";
40
+ import { createViewportRunGuard } from "../../core/self-heal/index.js";
41
+ import {
42
+ callScreeningLlm,
43
+ compactScreeningLlmResult,
44
+ createFailedLlmScreeningResult,
45
+ llmResultToScreening,
46
+ screenCandidate
47
+ } from "../../core/screening/index.js";
48
48
  import {
49
+ closeRecommendBlockingPanels,
49
50
  closeRecommendDetail,
50
51
  createRecommendDetailNetworkRecorder,
51
52
  extractRecommendDetailCandidate,
@@ -54,96 +55,96 @@ import {
54
55
  openRecommendCardDetailWithFreshRetry,
55
56
  waitForRecommendDetailNetworkEvents
56
57
  } from "./detail.js";
57
- import {
58
- readRecommendCardCandidate,
59
- waitForRecommendCardNodeIds
60
- } from "./cards.js";
61
- import { selectAndConfirmFirstSafeFilter } from "./filters.js";
62
- import {
63
- buildRecommendFilterSelectionOptions,
64
- refreshRecommendListAtEnd
65
- } from "./refresh.js";
66
- import { selectRecommendJob } from "./jobs.js";
67
- import {
68
- normalizeRecommendPageScope,
69
- selectRecommendPageScope
70
- } from "./scopes.js";
71
- import {
72
- RECOMMEND_BOTTOM_MARKER_SELECTORS,
73
- RECOMMEND_CARD_SELECTOR,
74
- RECOMMEND_END_REFRESH_SELECTOR,
75
- RECOMMEND_LIST_CONTAINER_SELECTORS,
76
- RECOMMEND_TARGET_URL
77
- } from "./constants.js";
78
- import {
79
- clickRecommendActionControl,
80
- normalizeRecommendPostAction,
81
- resolveRecommendPostAction,
82
- waitForRecommendDetailActionControls
83
- } from "./actions.js";
84
- import { getRecommendRoots } from "./roots.js";
85
-
86
- function normalizeLabels(labels = []) {
87
- return labels.map((label) => String(label || "").trim()).filter(Boolean);
88
- }
89
-
90
- function isRefreshableListStall(reason = "") {
91
- return new Set([
92
- "stable_visible_signature",
93
- "max_scrolls_exhausted",
94
- "scroll_failed",
95
- "scroll_anchor_unavailable"
96
- ]).has(String(reason || ""));
97
- }
98
-
99
- function normalizeFilter(filter = {}) {
100
- const filterGroups = Array.isArray(filter.filterGroups)
101
- ? filter.filterGroups
102
- : Array.isArray(filter.groups)
103
- ? filter.groups
104
- : [];
105
- return {
106
- enabled: filter.enabled !== false,
107
- group: String(filter.group || ""),
108
- labels: normalizeLabels(filter.labels || filter.filterLabels || []),
109
- selectAllLabels: Boolean(filter.selectAllLabels),
110
- filterGroups: filterGroups.map((group) => ({
111
- group: String(group?.group || ""),
112
- labels: normalizeLabels(group?.labels || group?.filterLabels || []),
113
- selectAllLabels: group?.selectAllLabels !== false
114
- })).filter((group) => group.group || group.labels.length)
115
- };
116
- }
117
-
118
- function compactFilterResult(filterResult) {
119
- if (!filterResult) return null;
120
- return {
121
- opened_panel: Boolean(filterResult.opened_panel),
122
- selected_option: filterResult.selected_option
123
- ? {
124
- group: filterResult.selected_option.group,
125
- label: filterResult.selected_option.label,
126
- was_active: Boolean(filterResult.selected_option.was_active),
127
- clicked: filterResult.selected_option.clicked !== false
128
- }
129
- : null,
130
- selected_options: (filterResult.selected_options || []).map((option) => ({
131
- group: option.group,
132
- label: option.label,
133
- was_active: Boolean(option.was_active),
134
- clicked: option.clicked !== false
135
- })),
136
- confirmed: Boolean(filterResult.confirmed),
137
- before_counts: filterResult.before_counts,
138
- after_confirm_counts: filterResult.after_confirm_counts
139
- };
140
- }
141
-
142
- function compactJobSelection(jobSelection) {
143
- if (!jobSelection) return null;
144
- return {
145
- requested: jobSelection.requested || "",
146
- selected: Boolean(jobSelection.selected),
58
+ import {
59
+ readRecommendCardCandidate,
60
+ waitForRecommendCardNodeIds
61
+ } from "./cards.js";
62
+ import { selectAndConfirmFirstSafeFilter } from "./filters.js";
63
+ import {
64
+ buildRecommendFilterSelectionOptions,
65
+ refreshRecommendListAtEnd
66
+ } from "./refresh.js";
67
+ import { selectRecommendJob } from "./jobs.js";
68
+ import {
69
+ normalizeRecommendPageScope,
70
+ selectRecommendPageScope
71
+ } from "./scopes.js";
72
+ import {
73
+ RECOMMEND_BOTTOM_MARKER_SELECTORS,
74
+ RECOMMEND_CARD_SELECTOR,
75
+ RECOMMEND_END_REFRESH_SELECTOR,
76
+ RECOMMEND_LIST_CONTAINER_SELECTORS,
77
+ RECOMMEND_TARGET_URL
78
+ } from "./constants.js";
79
+ import {
80
+ clickRecommendActionControl,
81
+ normalizeRecommendPostAction,
82
+ resolveRecommendPostAction,
83
+ waitForRecommendDetailActionControls
84
+ } from "./actions.js";
85
+ import { getRecommendRoots } from "./roots.js";
86
+
87
+ function normalizeLabels(labels = []) {
88
+ return labels.map((label) => String(label || "").trim()).filter(Boolean);
89
+ }
90
+
91
+ function isRefreshableListStall(reason = "") {
92
+ return new Set([
93
+ "stable_visible_signature",
94
+ "max_scrolls_exhausted",
95
+ "scroll_failed",
96
+ "scroll_anchor_unavailable"
97
+ ]).has(String(reason || ""));
98
+ }
99
+
100
+ function normalizeFilter(filter = {}) {
101
+ const filterGroups = Array.isArray(filter.filterGroups)
102
+ ? filter.filterGroups
103
+ : Array.isArray(filter.groups)
104
+ ? filter.groups
105
+ : [];
106
+ return {
107
+ enabled: filter.enabled !== false,
108
+ group: String(filter.group || ""),
109
+ labels: normalizeLabels(filter.labels || filter.filterLabels || []),
110
+ selectAllLabels: Boolean(filter.selectAllLabels),
111
+ filterGroups: filterGroups.map((group) => ({
112
+ group: String(group?.group || ""),
113
+ labels: normalizeLabels(group?.labels || group?.filterLabels || []),
114
+ selectAllLabels: group?.selectAllLabels !== false
115
+ })).filter((group) => group.group || group.labels.length)
116
+ };
117
+ }
118
+
119
+ function compactFilterResult(filterResult) {
120
+ if (!filterResult) return null;
121
+ return {
122
+ opened_panel: Boolean(filterResult.opened_panel),
123
+ selected_option: filterResult.selected_option
124
+ ? {
125
+ group: filterResult.selected_option.group,
126
+ label: filterResult.selected_option.label,
127
+ was_active: Boolean(filterResult.selected_option.was_active),
128
+ clicked: filterResult.selected_option.clicked !== false
129
+ }
130
+ : null,
131
+ selected_options: (filterResult.selected_options || []).map((option) => ({
132
+ group: option.group,
133
+ label: option.label,
134
+ was_active: Boolean(option.was_active),
135
+ clicked: option.clicked !== false
136
+ })),
137
+ confirmed: Boolean(filterResult.confirmed),
138
+ before_counts: filterResult.before_counts,
139
+ after_confirm_counts: filterResult.after_confirm_counts
140
+ };
141
+ }
142
+
143
+ function compactJobSelection(jobSelection) {
144
+ if (!jobSelection) return null;
145
+ return {
146
+ requested: jobSelection.requested || "",
147
+ selected: Boolean(jobSelection.selected),
147
148
  already_current: Boolean(jobSelection.already_current),
148
149
  reason: jobSelection.reason || null,
149
150
  selected_option: jobSelection.selected_option || null,
@@ -171,222 +172,222 @@ function compactJobSelection(jobSelection) {
171
172
  }
172
173
  : null,
173
174
  options: (jobSelection.options || []).map((option) => ({
174
- label: option.label,
175
- label_without_salary: option.label_without_salary,
176
- current: Boolean(option.current),
177
- visible: Boolean(option.visible),
178
- class_name: option.class_name
179
- }))
180
- };
181
- }
182
-
183
- function compactPageScopeSelection(pageScopeSelection) {
184
- if (!pageScopeSelection) return null;
185
- return {
186
- requested_scope: pageScopeSelection.requested_scope || null,
187
- effective_scope: pageScopeSelection.effective_scope || null,
188
- fallback_scope: pageScopeSelection.fallback_scope || null,
189
- fallback_applied: Boolean(pageScopeSelection.fallback_applied),
190
- selected: Boolean(pageScopeSelection.selected),
191
- already_current: Boolean(pageScopeSelection.already_current),
192
- reason: pageScopeSelection.reason || null,
193
- selected_tab: pageScopeSelection.selected_tab || null,
194
- available_scopes: pageScopeSelection.available_scopes || [],
195
- card_count: pageScopeSelection.after?.card_count || null
196
- };
197
- }
198
-
199
- function compactScreening(screening) {
200
- return {
201
- status: screening.status,
202
- passed: screening.passed,
203
- score: screening.score,
204
- reasons: screening.reasons,
205
- candidate: {
206
- domain: screening.candidate?.domain || "recommend",
207
- source: screening.candidate?.source || "",
208
- id: screening.candidate?.id || null,
209
- identity: screening.candidate?.identity || {}
210
- }
211
- };
212
- }
213
-
214
- function compactCandidate(candidate) {
215
- return {
216
- id: candidate?.id || null,
217
- identity: candidate?.identity || {},
218
- text_length: candidate?.text?.raw?.length || 0,
219
- tag_count: candidate?.tags?.length || 0
220
- };
221
- }
222
-
223
- function compactDetail(detailResult) {
224
- if (!detailResult) return null;
225
- return {
226
- popup_text_length: detailResult.detail?.popup_text?.length || 0,
227
- resume_text_length: detailResult.detail?.resume_text?.length || 0,
228
- network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
229
- parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
230
- cv_acquisition: detailResult.cv_acquisition || null,
231
- image_evidence: summarizeImageEvidence(detailResult.image_evidence),
232
- llm_screening: compactScreeningLlmResult(detailResult.llm_result),
233
- close_result: detailResult.close_result
234
- };
235
- }
236
-
237
- function normalizeScreeningMode(value) {
238
- const normalized = String(value || "llm").trim().toLowerCase();
239
- return ["deterministic", "local", "local_scorer"].includes(normalized)
240
- ? "deterministic"
241
- : "llm";
242
- }
243
-
244
- function createMissingLlmConfigResult() {
245
- return createFailedLlmScreeningResult(new Error("LLM screening config is required for production recommend runs"));
246
- }
247
-
248
- function compactActionDiscovery(discovery) {
249
- if (!discovery) return null;
250
- return {
251
- elapsed_ms: discovery.elapsed_ms,
252
- timed_out: Boolean(discovery.timed_out),
253
- detail_root_count: discovery.detail_root_count || 0,
254
- summary: discovery.summary || null
255
- };
256
- }
257
-
258
- async function runRecommendPostAction({
259
- client,
260
- screening,
261
- actionDiscovery,
262
- postAction = "none",
263
- greetCount = 0,
264
- maxGreetCount = null,
265
- executePostAction = true,
266
- afterClickDelayMs = 900
267
- } = {}) {
268
- const plan = resolveRecommendPostAction({
269
- postAction,
270
- greetCount,
271
- maxGreetCount
272
- });
273
- const result = {
274
- requested: postAction,
275
- execute_post_action: Boolean(executePostAction),
276
- plan,
277
- eligible: Boolean(screening?.passed),
278
- action_attempted: false,
279
- action_clicked: false,
280
- counted_as_greet: false,
281
- reason: ""
282
- };
283
-
284
- if (!screening?.passed) {
285
- result.reason = "screening_not_passed";
286
- return result;
287
- }
288
- if (plan.effective === "none") {
289
- result.reason = "post_action_none";
290
- return result;
291
- }
292
-
293
- const summary = actionDiscovery?.summary || {};
294
- const control = plan.effective === "favorite" ? summary.favorite : summary.greet;
295
- if (!control?.found) {
296
- result.reason = `${plan.effective}_control_not_found`;
297
- return result;
298
- }
299
- result.control = control;
300
-
301
- if (plan.effective === "greet" && control.continue_chat) {
302
- result.reason = "already_connected_continue_chat";
303
- result.already_connected = true;
304
- return result;
305
- }
306
- if (plan.effective === "greet" && control.greet_quota?.exhausted) {
307
- result.reason = "greet_credits_exhausted";
308
- result.out_of_greet_credits = true;
309
- result.stop_run = true;
310
- return result;
311
- }
312
- if (plan.effective === "greet" && control.available === false) {
313
- result.reason = "greet_control_not_available";
314
- return result;
315
- }
316
- if (plan.effective === "favorite" && control.active) {
317
- result.reason = "already_favorited";
318
- result.already_favorited = true;
319
- return result;
320
- }
321
- if (control.disabled) {
322
- result.reason = `${plan.effective}_control_disabled`;
323
- return result;
324
- }
325
- if (!executePostAction) {
326
- result.reason = "dry_run_post_action";
327
- result.would_click = true;
328
- return result;
329
- }
330
-
331
- result.action_attempted = true;
332
- result.control_before = control;
333
- let clickResult;
334
- try {
335
- clickResult = await clickRecommendActionControl(client, {
336
- ...control,
337
- kind: plan.effective
338
- });
339
- } catch (error) {
340
- if (error?.code === GREET_CREDITS_EXHAUSTED_CODE) {
341
- result.reason = "greet_credits_exhausted";
342
- result.out_of_greet_credits = true;
343
- result.stop_run = true;
344
- result.greet_quota = error.greet_quota || control.greet_quota || null;
345
- return result;
346
- }
347
- throw error;
348
- }
349
- result.click_result = clickResult;
350
- result.action_clicked = true;
351
- result.counted_as_greet = plan.effective === "greet";
352
- result.reason = "clicked";
353
- if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
354
- try {
355
- const afterDiscovery = await waitForRecommendDetailActionControls(client, {
356
- timeoutMs: 2500,
357
- intervalMs: 300,
358
- requireAny: false
359
- });
360
- const afterSummary = afterDiscovery?.summary || {};
361
- const afterControl = plan.effective === "favorite" ? afterSummary.favorite : afterSummary.greet;
362
- result.action_discovery_after = compactActionDiscovery(afterDiscovery);
363
- result.control_after = afterControl || null;
364
- if (plan.effective === "greet") {
365
- result.verified_after_click = Boolean(
366
- afterControl?.continue_chat
367
- || String(afterControl?.label || "").includes("继续沟通")
368
- );
369
- } else if (plan.effective === "favorite") {
370
- result.verified_after_click = Boolean(
371
- afterControl?.active
372
- || String(afterControl?.label || "").includes("已")
373
- );
374
- }
375
- } catch (error) {
376
- result.verify_error = {
377
- message: error?.message || String(error)
378
- };
379
- }
380
- return result;
381
- }
382
-
383
- function compactRefreshAttempt(refreshAttempt) {
384
- if (!refreshAttempt) return null;
385
- return {
386
- ok: Boolean(refreshAttempt.ok),
387
- method: refreshAttempt.method || "",
388
- reason: refreshAttempt.reason || null,
389
- error: refreshAttempt.error || null,
175
+ label: option.label,
176
+ label_without_salary: option.label_without_salary,
177
+ current: Boolean(option.current),
178
+ visible: Boolean(option.visible),
179
+ class_name: option.class_name
180
+ }))
181
+ };
182
+ }
183
+
184
+ function compactPageScopeSelection(pageScopeSelection) {
185
+ if (!pageScopeSelection) return null;
186
+ return {
187
+ requested_scope: pageScopeSelection.requested_scope || null,
188
+ effective_scope: pageScopeSelection.effective_scope || null,
189
+ fallback_scope: pageScopeSelection.fallback_scope || null,
190
+ fallback_applied: Boolean(pageScopeSelection.fallback_applied),
191
+ selected: Boolean(pageScopeSelection.selected),
192
+ already_current: Boolean(pageScopeSelection.already_current),
193
+ reason: pageScopeSelection.reason || null,
194
+ selected_tab: pageScopeSelection.selected_tab || null,
195
+ available_scopes: pageScopeSelection.available_scopes || [],
196
+ card_count: pageScopeSelection.after?.card_count || null
197
+ };
198
+ }
199
+
200
+ function compactScreening(screening) {
201
+ return {
202
+ status: screening.status,
203
+ passed: screening.passed,
204
+ score: screening.score,
205
+ reasons: screening.reasons,
206
+ candidate: {
207
+ domain: screening.candidate?.domain || "recommend",
208
+ source: screening.candidate?.source || "",
209
+ id: screening.candidate?.id || null,
210
+ identity: screening.candidate?.identity || {}
211
+ }
212
+ };
213
+ }
214
+
215
+ function compactCandidate(candidate) {
216
+ return {
217
+ id: candidate?.id || null,
218
+ identity: candidate?.identity || {},
219
+ text_length: candidate?.text?.raw?.length || 0,
220
+ tag_count: candidate?.tags?.length || 0
221
+ };
222
+ }
223
+
224
+ function compactDetail(detailResult) {
225
+ if (!detailResult) return null;
226
+ return {
227
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
228
+ resume_text_length: detailResult.detail?.resume_text?.length || 0,
229
+ network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
230
+ parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
231
+ cv_acquisition: detailResult.cv_acquisition || null,
232
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
233
+ llm_screening: compactScreeningLlmResult(detailResult.llm_result),
234
+ close_result: detailResult.close_result
235
+ };
236
+ }
237
+
238
+ function normalizeScreeningMode(value) {
239
+ const normalized = String(value || "llm").trim().toLowerCase();
240
+ return ["deterministic", "local", "local_scorer"].includes(normalized)
241
+ ? "deterministic"
242
+ : "llm";
243
+ }
244
+
245
+ function createMissingLlmConfigResult() {
246
+ return createFailedLlmScreeningResult(new Error("LLM screening config is required for production recommend runs"));
247
+ }
248
+
249
+ function compactActionDiscovery(discovery) {
250
+ if (!discovery) return null;
251
+ return {
252
+ elapsed_ms: discovery.elapsed_ms,
253
+ timed_out: Boolean(discovery.timed_out),
254
+ detail_root_count: discovery.detail_root_count || 0,
255
+ summary: discovery.summary || null
256
+ };
257
+ }
258
+
259
+ async function runRecommendPostAction({
260
+ client,
261
+ screening,
262
+ actionDiscovery,
263
+ postAction = "none",
264
+ greetCount = 0,
265
+ maxGreetCount = null,
266
+ executePostAction = true,
267
+ afterClickDelayMs = 900
268
+ } = {}) {
269
+ const plan = resolveRecommendPostAction({
270
+ postAction,
271
+ greetCount,
272
+ maxGreetCount
273
+ });
274
+ const result = {
275
+ requested: postAction,
276
+ execute_post_action: Boolean(executePostAction),
277
+ plan,
278
+ eligible: Boolean(screening?.passed),
279
+ action_attempted: false,
280
+ action_clicked: false,
281
+ counted_as_greet: false,
282
+ reason: ""
283
+ };
284
+
285
+ if (!screening?.passed) {
286
+ result.reason = "screening_not_passed";
287
+ return result;
288
+ }
289
+ if (plan.effective === "none") {
290
+ result.reason = "post_action_none";
291
+ return result;
292
+ }
293
+
294
+ const summary = actionDiscovery?.summary || {};
295
+ const control = plan.effective === "favorite" ? summary.favorite : summary.greet;
296
+ if (!control?.found) {
297
+ result.reason = `${plan.effective}_control_not_found`;
298
+ return result;
299
+ }
300
+ result.control = control;
301
+
302
+ if (plan.effective === "greet" && control.continue_chat) {
303
+ result.reason = "already_connected_continue_chat";
304
+ result.already_connected = true;
305
+ return result;
306
+ }
307
+ if (plan.effective === "greet" && control.greet_quota?.exhausted) {
308
+ result.reason = "greet_credits_exhausted";
309
+ result.out_of_greet_credits = true;
310
+ result.stop_run = true;
311
+ return result;
312
+ }
313
+ if (plan.effective === "greet" && control.available === false) {
314
+ result.reason = "greet_control_not_available";
315
+ return result;
316
+ }
317
+ if (plan.effective === "favorite" && control.active) {
318
+ result.reason = "already_favorited";
319
+ result.already_favorited = true;
320
+ return result;
321
+ }
322
+ if (control.disabled) {
323
+ result.reason = `${plan.effective}_control_disabled`;
324
+ return result;
325
+ }
326
+ if (!executePostAction) {
327
+ result.reason = "dry_run_post_action";
328
+ result.would_click = true;
329
+ return result;
330
+ }
331
+
332
+ result.action_attempted = true;
333
+ result.control_before = control;
334
+ let clickResult;
335
+ try {
336
+ clickResult = await clickRecommendActionControl(client, {
337
+ ...control,
338
+ kind: plan.effective
339
+ });
340
+ } catch (error) {
341
+ if (error?.code === GREET_CREDITS_EXHAUSTED_CODE) {
342
+ result.reason = "greet_credits_exhausted";
343
+ result.out_of_greet_credits = true;
344
+ result.stop_run = true;
345
+ result.greet_quota = error.greet_quota || control.greet_quota || null;
346
+ return result;
347
+ }
348
+ throw error;
349
+ }
350
+ result.click_result = clickResult;
351
+ result.action_clicked = true;
352
+ result.counted_as_greet = plan.effective === "greet";
353
+ result.reason = "clicked";
354
+ if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
355
+ try {
356
+ const afterDiscovery = await waitForRecommendDetailActionControls(client, {
357
+ timeoutMs: 2500,
358
+ intervalMs: 300,
359
+ requireAny: false
360
+ });
361
+ const afterSummary = afterDiscovery?.summary || {};
362
+ const afterControl = plan.effective === "favorite" ? afterSummary.favorite : afterSummary.greet;
363
+ result.action_discovery_after = compactActionDiscovery(afterDiscovery);
364
+ result.control_after = afterControl || null;
365
+ if (plan.effective === "greet") {
366
+ result.verified_after_click = Boolean(
367
+ afterControl?.continue_chat
368
+ || String(afterControl?.label || "").includes("继续沟通")
369
+ );
370
+ } else if (plan.effective === "favorite") {
371
+ result.verified_after_click = Boolean(
372
+ afterControl?.active
373
+ || String(afterControl?.label || "").includes("已")
374
+ );
375
+ }
376
+ } catch (error) {
377
+ result.verify_error = {
378
+ message: error?.message || String(error)
379
+ };
380
+ }
381
+ return result;
382
+ }
383
+
384
+ function compactRefreshAttempt(refreshAttempt) {
385
+ if (!refreshAttempt) return null;
386
+ return {
387
+ ok: Boolean(refreshAttempt.ok),
388
+ method: refreshAttempt.method || "",
389
+ reason: refreshAttempt.reason || null,
390
+ error: refreshAttempt.error || null,
390
391
  forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
391
392
  target_url: refreshAttempt.target_url || null,
392
393
  card_count: refreshAttempt.card_count || 0,
@@ -400,11 +401,11 @@ function compactRefreshAttempt(refreshAttempt) {
400
401
  }
401
402
  : null,
402
403
  attempts: (refreshAttempt.attempts || []).map((attempt) => ({
403
- ok: Boolean(attempt.ok),
404
- method: attempt.method || "",
405
- reason: attempt.reason || null,
406
- error: attempt.error || null,
407
- label: attempt.label || null,
404
+ ok: Boolean(attempt.ok),
405
+ method: attempt.method || "",
406
+ reason: attempt.reason || null,
407
+ error: attempt.error || null,
408
+ label: attempt.label || null,
408
409
  before_card_count: attempt.before_card_count || 0,
409
410
  after_card_count: attempt.after_card_count || 0,
410
411
  card_count: attempt.card_count || 0,
@@ -432,22 +433,22 @@ function compactRefreshAttempt(refreshAttempt) {
432
433
  filter: compactFilterResult(refreshAttempt.filter)
433
434
  };
434
435
  }
435
-
436
- export function countRecommendResultStatuses(results = [], {
437
- greetCount = 0
438
- } = {}) {
439
- return {
440
- processed: results.length,
441
- screened: results.length,
442
- detail_opened: results.filter((item) => item.detail).length,
443
- passed: results.filter((item) => item.screening?.passed).length,
444
- llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
445
- greet_count: greetCount,
446
- post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
447
- image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
448
- detail_open_failed: results.filter((item) => (
449
- item.error?.code === "DETAIL_STALE_NODE"
450
- || item.error?.code === "DETAIL_OPEN_FAILED"
436
+
437
+ export function countRecommendResultStatuses(results = [], {
438
+ greetCount = 0
439
+ } = {}) {
440
+ return {
441
+ processed: results.length,
442
+ screened: results.length,
443
+ detail_opened: results.filter((item) => item.detail).length,
444
+ passed: results.filter((item) => item.screening?.passed).length,
445
+ llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
446
+ greet_count: greetCount,
447
+ post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
448
+ image_capture_failed: results.filter((item) => item.detail?.image_evidence?.ok === false).length,
449
+ detail_open_failed: results.filter((item) => (
450
+ item.error?.code === "DETAIL_STALE_NODE"
451
+ || item.error?.code === "DETAIL_OPEN_FAILED"
451
452
  )).length,
452
453
  transient_recovered: results.filter((item) => (
453
454
  item.error?.code === "DETAIL_STALE_NODE"
@@ -455,22 +456,27 @@ export function countRecommendResultStatuses(results = [], {
455
456
  || item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
456
457
  || item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
457
458
  || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
458
- )).length
459
- };
460
- }
461
-
459
+ )).length
460
+ };
461
+ }
462
+
462
463
  function countPassedResults(results = []) {
463
464
  return countRecommendResultStatuses(results).passed;
464
465
  }
465
466
 
466
467
  function compactCloseResult(closeResult) {
467
468
  if (!closeResult) return null;
468
- return {
469
+ const result = {
469
470
  closed: Boolean(closeResult.closed),
470
471
  reason: closeResult.reason || null,
472
+ probe: closeResult.probe || null,
471
473
  attempts: closeResult.attempts || [],
472
474
  verification: closeResult.verification || null
473
475
  };
476
+ if (closeResult.already_closed !== undefined) {
477
+ result.already_closed = Boolean(closeResult.already_closed);
478
+ }
479
+ return result;
474
480
  }
475
481
 
476
482
  function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
@@ -482,6 +488,9 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
482
488
  if (error.close_result) {
483
489
  result.close_result = compactCloseResult(error.close_result);
484
490
  }
491
+ if (error.phase) {
492
+ result.phase = error.phase;
493
+ }
485
494
  if (error.refresh_attempt) {
486
495
  result.refresh_attempt = error.refresh_attempt;
487
496
  }
@@ -499,7 +508,7 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
499
508
  }
500
509
  return result;
501
510
  }
502
-
511
+
503
512
  function createRecommendCloseFailureError(closeResult) {
504
513
  const error = new Error(closeResult?.reason || "Recommend detail did not close before recovery");
505
514
  error.code = "DETAIL_CLOSE_FAILED";
@@ -507,6 +516,14 @@ function createRecommendCloseFailureError(closeResult) {
507
516
  return error;
508
517
  }
509
518
 
519
+ function createRecommendBlockingPanelCloseFailureError(closeResult, phase = "") {
520
+ const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
521
+ error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
522
+ error.close_result = closeResult || null;
523
+ error.phase = phase || null;
524
+ return error;
525
+ }
526
+
510
527
  function createRecommendRefreshFailureError(refreshAttempt, {
511
528
  listEndReason = "",
512
529
  targetCount = 0,
@@ -522,124 +539,124 @@ function createRecommendRefreshFailureError(refreshAttempt, {
522
539
  error.passed_count = passedCount;
523
540
  return error;
524
541
  }
525
-
526
- export function isRecoverableImageCaptureError(error) {
527
- const code = String(error?.code || "");
528
- if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
529
- if (isStaleRecommendNodeError(error)) return true;
530
- return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
531
- }
532
-
533
- function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
534
- const resolved = String(basePath || "").trim();
535
- if (!resolved) return [];
536
- const parsed = path.parse(resolved);
537
- const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
538
- const files = [];
539
- for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
540
- const page = String(index + 1).padStart(2, "0");
541
- const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
542
- if (fs.existsSync(candidatePath)) files.push(candidatePath);
543
- }
544
- return files;
545
- }
546
-
547
- export function createRecoverableImageCaptureEvidence(error, {
548
- elapsedMs = 0,
549
- filePath = "",
550
- extension = "jpg",
551
- maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
552
- } = {}) {
553
- const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
554
- return {
555
- schema_version: 1,
556
- ok: false,
557
- source: "image-scroll-sequence",
558
- elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
559
- capture_count: filePaths.length,
560
- screenshot_count: filePaths.length,
561
- unique_screenshot_count: filePaths.length,
562
- dropped_duplicate_count: 0,
563
- total_byte_length: 0,
564
- original_total_byte_length: 0,
565
- llm_screenshot_count: 0,
566
- llm_total_byte_length: 0,
567
- llm_original_total_byte_length: 0,
568
- llm_composition_error: null,
569
- error_code: error?.code || (isStaleRecommendNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
570
- error: error?.message || String(error || "Image capture failed"),
571
- file_paths: filePaths,
572
- llm_file_paths: []
573
- };
574
- }
575
-
576
- function createImageCaptureFailureScreening(candidate, error) {
577
- return {
578
- status: "fail",
579
- passed: false,
580
- score: 0,
581
- reasons: ["image_capture_failed"],
582
- error: compactError(error, "IMAGE_CAPTURE_FAILED"),
583
- candidate
584
- };
585
- }
586
-
542
+
543
+ export function isRecoverableImageCaptureError(error) {
544
+ const code = String(error?.code || "");
545
+ if (code === "IMAGE_CAPTURE_TIMEOUT" || code === "IMAGE_CAPTURE_TOTAL_TIMEOUT") return true;
546
+ if (isStaleRecommendNodeError(error)) return true;
547
+ return /Image fallback capture timed out/i.test(String(error?.message || error || ""));
548
+ }
549
+
550
+ function collectPartialImageEvidencePaths(basePath = "", extension = "jpg", maxCount = 12) {
551
+ const resolved = String(basePath || "").trim();
552
+ if (!resolved) return [];
553
+ const parsed = path.parse(resolved);
554
+ const ext = parsed.ext || `.${String(extension || "jpg").replace(/^\./, "") || "jpg"}`;
555
+ const files = [];
556
+ for (let index = 0; index < Math.max(1, Number(maxCount) || 1); index += 1) {
557
+ const page = String(index + 1).padStart(2, "0");
558
+ const candidatePath = path.join(parsed.dir, `${parsed.name}-page-${page}${ext}`);
559
+ if (fs.existsSync(candidatePath)) files.push(candidatePath);
560
+ }
561
+ return files;
562
+ }
563
+
564
+ export function createRecoverableImageCaptureEvidence(error, {
565
+ elapsedMs = 0,
566
+ filePath = "",
567
+ extension = "jpg",
568
+ maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
569
+ } = {}) {
570
+ const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
571
+ return {
572
+ schema_version: 1,
573
+ ok: false,
574
+ source: "image-scroll-sequence",
575
+ elapsed_ms: Math.max(0, Math.round(Number(error?.elapsed_ms ?? elapsedMs) || 0)),
576
+ capture_count: filePaths.length,
577
+ screenshot_count: filePaths.length,
578
+ unique_screenshot_count: filePaths.length,
579
+ dropped_duplicate_count: 0,
580
+ total_byte_length: 0,
581
+ original_total_byte_length: 0,
582
+ llm_screenshot_count: 0,
583
+ llm_total_byte_length: 0,
584
+ llm_original_total_byte_length: 0,
585
+ llm_composition_error: null,
586
+ error_code: error?.code || (isStaleRecommendNodeError(error) ? "IMAGE_CAPTURE_STALE_NODE" : "IMAGE_CAPTURE_FAILED"),
587
+ error: error?.message || String(error || "Image capture failed"),
588
+ file_paths: filePaths,
589
+ llm_file_paths: []
590
+ };
591
+ }
592
+
593
+ function createImageCaptureFailureScreening(candidate, error) {
594
+ return {
595
+ status: "fail",
596
+ passed: false,
597
+ score: 0,
598
+ reasons: ["image_capture_failed"],
599
+ error: compactError(error, "IMAGE_CAPTURE_FAILED"),
600
+ candidate
601
+ };
602
+ }
603
+
587
604
  export function isRecoverableRecommendDetailError(error) {
588
605
  return isStaleRecommendNodeError(error) || isRecommendDetailOpenMissError(error);
589
606
  }
590
-
591
- function compactRecoverableDetailError(error) {
592
- return compactError(error, isStaleRecommendNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
593
- }
594
-
595
- function createRecoverableDetailFailureScreening(candidate, error) {
596
- return {
597
- status: "fail",
598
- passed: false,
607
+
608
+ function compactRecoverableDetailError(error) {
609
+ return compactError(error, isStaleRecommendNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
610
+ }
611
+
612
+ function createRecoverableDetailFailureScreening(candidate, error) {
613
+ return {
614
+ status: "fail",
615
+ passed: false,
599
616
  score: 0,
600
617
  reasons: isStaleRecommendNodeError(error)
601
618
  ? ["detail_open_failed", "stale_node"]
602
619
  : isRecommendDetailOpenMissError(error)
603
620
  ? ["detail_open_failed", "detail_open_miss"]
604
621
  : ["detail_open_failed"],
605
- error: compactRecoverableDetailError(error),
606
- candidate
607
- };
608
- }
609
-
610
- export async function runRecommendWorkflow({
611
- client,
612
- targetUrl = "",
613
- criteria = "",
614
- jobLabel = "",
615
- pageScope = "recommend",
616
- fallbackPageScope = "recommend",
617
- filter = {},
618
- maxCandidates = 5,
619
- detailLimit,
620
- closeDetail = true,
621
- delayMs = 0,
622
- cardTimeoutMs = 10000,
623
- maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
624
- imageWheelDeltaY = 650,
625
- cvAcquisitionMode = "unknown",
626
- listMaxScrolls = 20,
627
- listStableSignatureLimit = 5,
628
- listWheelDeltaY = 850,
629
- listSettleMs = 2200,
630
- listFallbackPoint = null,
631
- refreshOnEnd = true,
632
- maxRefreshRounds = 2,
633
- refreshButtonSettleMs = 8000,
634
- refreshReloadSettleMs = 8000,
635
- postAction = "none",
636
- maxGreetCount = null,
637
- executePostAction = true,
638
- actionTimeoutMs = 8000,
639
- actionIntervalMs = 500,
640
- actionAfterClickDelayMs = 900,
641
- screeningMode = "llm",
642
- llmConfig = null,
622
+ error: compactRecoverableDetailError(error),
623
+ candidate
624
+ };
625
+ }
626
+
627
+ export async function runRecommendWorkflow({
628
+ client,
629
+ targetUrl = "",
630
+ criteria = "",
631
+ jobLabel = "",
632
+ pageScope = "recommend",
633
+ fallbackPageScope = "recommend",
634
+ filter = {},
635
+ maxCandidates = 5,
636
+ detailLimit,
637
+ closeDetail = true,
638
+ delayMs = 0,
639
+ cardTimeoutMs = 10000,
640
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
641
+ imageWheelDeltaY = 650,
642
+ cvAcquisitionMode = "unknown",
643
+ listMaxScrolls = 20,
644
+ listStableSignatureLimit = 5,
645
+ listWheelDeltaY = 850,
646
+ listSettleMs = 2200,
647
+ listFallbackPoint = null,
648
+ refreshOnEnd = true,
649
+ maxRefreshRounds = 2,
650
+ refreshButtonSettleMs = 8000,
651
+ refreshReloadSettleMs = 8000,
652
+ postAction = "none",
653
+ maxGreetCount = null,
654
+ executePostAction = true,
655
+ actionTimeoutMs = 8000,
656
+ actionIntervalMs = 500,
657
+ actionAfterClickDelayMs = 900,
658
+ screeningMode = "llm",
659
+ llmConfig = null,
643
660
  llmTimeoutMs = 120000,
644
661
  llmImageLimit = 8,
645
662
  llmImageDetail = "high",
@@ -665,54 +682,55 @@ export async function runRecommendWorkflow({
665
682
  batchRestEnabled: effectiveHumanBehavior.batchRest
666
683
  });
667
684
  const normalizedFilter = normalizeFilter(filter);
668
- const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
669
- const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
670
- const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
671
- const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
672
- const useLlmScreening = normalizedScreeningMode !== "deterministic";
673
- const postActionEnabled = normalizedPostAction !== "none";
674
- const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
675
- const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
676
- const effectiveDetailLimit = postActionEnabled ? Number.POSITIVE_INFINITY : detailCountLimit;
677
- const networkRecorder = effectiveDetailLimit > 0
678
- ? createRecommendDetailNetworkRecorder(client)
679
- : null;
680
- const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
681
- const listState = createInfiniteListState({
682
- domain: "recommend",
683
- listName: "recommend-candidates"
684
- });
685
- const viewportGuard = createViewportRunGuard({
686
- client,
687
- domain: "recommend",
688
- root: "frame",
689
- frameOwnerRoot: "frameOwner",
690
- runControl,
691
- getRoots: getRecommendRoots
692
- });
693
- async function ensureRecommendViewport(rootState, phase) {
694
- const result = await viewportGuard.ensure(rootState, { phase });
695
- return result.rootState || rootState;
696
- }
697
- const results = [];
698
- const refreshAttempts = [];
699
- let refreshRounds = 0;
700
- let contextRecoveryAttempts = 0;
701
- let greetCount = 0;
685
+ const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
686
+ const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
687
+ const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
688
+ const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
689
+ const useLlmScreening = normalizedScreeningMode !== "deterministic";
690
+ const postActionEnabled = normalizedPostAction !== "none";
691
+ const targetPassCount = Math.max(1, Number(maxCandidates) || 1);
692
+ const detailCountLimit = detailLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, Number(detailLimit) || 0);
693
+ const effectiveDetailLimit = postActionEnabled ? Number.POSITIVE_INFINITY : detailCountLimit;
694
+ const networkRecorder = effectiveDetailLimit > 0
695
+ ? createRecommendDetailNetworkRecorder(client)
696
+ : null;
697
+ const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
698
+ const listState = createInfiniteListState({
699
+ domain: "recommend",
700
+ listName: "recommend-candidates"
701
+ });
702
+ const viewportGuard = createViewportRunGuard({
703
+ client,
704
+ domain: "recommend",
705
+ root: "frame",
706
+ frameOwnerRoot: "frameOwner",
707
+ runControl,
708
+ getRoots: getRecommendRoots
709
+ });
710
+ async function ensureRecommendViewport(rootState, phase) {
711
+ const result = await viewportGuard.ensure(rootState, { phase });
712
+ return result.rootState || rootState;
713
+ }
714
+ const results = [];
715
+ const refreshAttempts = [];
716
+ let refreshRounds = 0;
717
+ let contextRecoveryAttempts = 0;
718
+ let greetCount = 0;
702
719
  const candidateRecoveryCounts = new Map();
703
720
  let jobSelection = null;
704
721
  let pageScopeSelection = null;
705
722
  let filterResult = null;
723
+ let rootState = null;
706
724
  let cardNodeIds = [];
707
725
  let listEndReason = "";
708
726
  let lastHumanEvent = null;
709
727
  const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
710
- rootNodeId: rootState?.iframe?.documentNodeId,
711
- containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
712
- itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
713
- itemSelectors: [RECOMMEND_CARD_SELECTOR],
714
- viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
715
- validateViewportPoint: true
728
+ rootNodeId: rootState?.iframe?.documentNodeId,
729
+ containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
730
+ itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
731
+ itemSelectors: [RECOMMEND_CARD_SELECTOR],
732
+ viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
733
+ validateViewportPoint: true
716
734
  }));
717
735
 
718
736
  function recordHumanEvent(event = null) {
@@ -749,13 +767,13 @@ export async function runRecommendWorkflow({
749
767
  card_count: cardNodeIds.length,
750
768
  target_count: targetPassCount,
751
769
  target_count_semantics: "passed_candidates",
752
- ...counts,
753
- screening_mode: normalizedScreeningMode,
754
- unique_seen: listSnapshot.seen_count,
755
- scroll_count: listSnapshot.scroll_count,
756
- refresh_rounds: refreshRounds,
757
- refresh_attempts: refreshAttempts.length,
758
- context_recoveries: contextRecoveryAttempts,
770
+ ...counts,
771
+ screening_mode: normalizedScreeningMode,
772
+ unique_seen: listSnapshot.seen_count,
773
+ scroll_count: listSnapshot.scroll_count,
774
+ refresh_rounds: refreshRounds,
775
+ refresh_attempts: refreshAttempts.length,
776
+ context_recoveries: contextRecoveryAttempts,
759
777
  list_end_reason: listEndReason || null,
760
778
  viewport_checks: viewportGuard.getStats().checks,
761
779
  viewport_recoveries: viewportGuard.getStats().recoveries,
@@ -768,280 +786,305 @@ export async function runRecommendWorkflow({
768
786
  ...extra
769
787
  });
770
788
  }
771
-
789
+
772
790
  function checkpointInProgressCandidate({
773
791
  index = results.length,
774
792
  candidateKey = "",
775
- cardNodeId = null,
776
- detailStep = "",
777
- error = null
778
- } = {}) {
779
- runControl.checkpoint({
780
- in_progress_candidate: {
781
- index,
782
- key: candidateKey,
783
- card_node_id: cardNodeId,
784
- detail_step: detailStep || null,
785
- counters: countRecommendResultStatuses(results, { greetCount }),
786
- error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
787
- },
793
+ cardNodeId = null,
794
+ detailStep = "",
795
+ error = null
796
+ } = {}) {
797
+ runControl.checkpoint({
798
+ in_progress_candidate: {
799
+ index,
800
+ key: candidateKey,
801
+ card_node_id: cardNodeId,
802
+ detail_step: detailStep || null,
803
+ counters: countRecommendResultStatuses(results, { greetCount }),
804
+ error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
805
+ },
788
806
  candidate_list: compactInfiniteListState(listState)
789
807
  });
790
808
  }
791
809
 
810
+ async function closeRecommendBlockingPanelsForRun(phase = "cleanup") {
811
+ const result = await closeRecommendBlockingPanels(client, {
812
+ attemptsLimit: 2,
813
+ rootState
814
+ });
815
+ if (!result?.closed) {
816
+ throw createRecommendBlockingPanelCloseFailureError(result, phase);
817
+ }
818
+ return result;
819
+ }
820
+
792
821
  async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
793
- forceRecentNotView = true
794
- } = {}) {
795
- await runControl.waitIfPaused();
796
- runControl.throwIfCanceled();
797
- const started = Date.now();
798
- runControl.setPhase("recommend:recover-context");
799
- contextRecoveryAttempts += 1;
822
+ forceRecentNotView = true
823
+ } = {}) {
824
+ await runControl.waitIfPaused();
825
+ runControl.throwIfCanceled();
826
+ const started = Date.now();
827
+ runControl.setPhase("recommend:recover-context");
828
+ contextRecoveryAttempts += 1;
800
829
  const refreshResult = await refreshRecommendListAtEnd(client, {
801
830
  rootState,
802
831
  jobLabel,
803
- pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
804
- fallbackPageScope: normalizedFallbackPageScope,
805
- filter: normalizedFilter,
806
- preferEndRefreshButton: false,
807
- forceNavigate: true,
808
- targetUrl: targetUrl || RECOMMEND_TARGET_URL,
809
- forceRecentNotView,
810
- cardTimeoutMs,
832
+ pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
833
+ fallbackPageScope: normalizedFallbackPageScope,
834
+ filter: normalizedFilter,
835
+ preferEndRefreshButton: false,
836
+ forceNavigate: true,
837
+ targetUrl: targetUrl || RECOMMEND_TARGET_URL,
838
+ forceRecentNotView,
839
+ cardTimeoutMs,
811
840
  buttonSettleMs: refreshButtonSettleMs,
812
841
  reloadSettleMs: refreshReloadSettleMs
813
842
  });
843
+ let blockingPanelClose = null;
844
+ if (refreshResult.ok) {
845
+ blockingPanelClose = await closeRecommendBlockingPanels(client, {
846
+ attemptsLimit: 2,
847
+ rootState: refreshResult.root_state || rootState
848
+ });
849
+ }
814
850
  const compactRefresh = {
815
851
  ...compactRefreshAttempt(refreshResult),
816
852
  context_recovery: true,
817
853
  recovery_reason: reason,
818
854
  trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
855
+ account_rights_panel_close: compactCloseResult(blockingPanelClose),
819
856
  elapsed_ms: Date.now() - started
820
857
  };
821
- refreshAttempts.push(compactRefresh);
822
- runControl.checkpoint({
823
- context_recovery: {
824
- attempt: contextRecoveryAttempts,
825
- reason,
826
- trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
827
- refresh: compactRefresh,
828
- counters: countRecommendResultStatuses(results, { greetCount })
829
- },
830
- candidate_list: compactInfiniteListState(listState)
831
- });
832
- if (!refreshResult.ok) {
833
- updateRecommendProgress({
834
- refresh_method: refreshResult.method || null,
835
- refresh_forced_recent_not_view: forceRecentNotView,
836
- recovery_reason: reason
858
+ refreshAttempts.push(compactRefresh);
859
+ runControl.checkpoint({
860
+ context_recovery: {
861
+ attempt: contextRecoveryAttempts,
862
+ reason,
863
+ trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
864
+ refresh: compactRefresh,
865
+ counters: countRecommendResultStatuses(results, { greetCount })
866
+ },
867
+ candidate_list: compactInfiniteListState(listState)
868
+ });
869
+ if (!refreshResult.ok) {
870
+ updateRecommendProgress({
871
+ refresh_method: refreshResult.method || null,
872
+ refresh_forced_recent_not_view: forceRecentNotView,
873
+ recovery_reason: reason
837
874
  });
838
875
  throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
839
876
  }
877
+ if (!blockingPanelClose?.closed) {
878
+ const panelError = createRecommendBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
879
+ panelError.refresh_attempt = compactRefresh;
880
+ throw panelError;
881
+ }
840
882
  rootState = refreshResult.root_state || await getRecommendRoots(client);
841
- rootState = await ensureRecommendViewport(rootState, "recover_after");
842
- cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
843
- timeoutMs: cardTimeoutMs,
844
- intervalMs: 300
845
- });
846
- resetInfiniteListForRefreshRound(listState, {
847
- reason: `context_recovery:${reason}`,
848
- round: contextRecoveryAttempts,
849
- method: refreshResult.method,
850
- metadata: {
851
- card_count: cardNodeIds.length,
852
- forced_recent_not_view: forceRecentNotView,
853
- counters: countRecommendResultStatuses(results, { greetCount })
854
- }
855
- });
856
- listEndReason = "";
857
- updateRecommendProgress({
858
- card_count: cardNodeIds.length,
859
- refresh_method: refreshResult.method || null,
860
- refresh_forced_recent_not_view: forceRecentNotView,
861
- recovery_reason: reason
862
- });
863
- return refreshResult;
864
- }
865
-
883
+ rootState = await ensureRecommendViewport(rootState, "recover_after");
884
+ cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
885
+ timeoutMs: cardTimeoutMs,
886
+ intervalMs: 300
887
+ });
888
+ resetInfiniteListForRefreshRound(listState, {
889
+ reason: `context_recovery:${reason}`,
890
+ round: contextRecoveryAttempts,
891
+ method: refreshResult.method,
892
+ metadata: {
893
+ card_count: cardNodeIds.length,
894
+ forced_recent_not_view: forceRecentNotView,
895
+ counters: countRecommendResultStatuses(results, { greetCount })
896
+ }
897
+ });
898
+ listEndReason = "";
899
+ updateRecommendProgress({
900
+ card_count: cardNodeIds.length,
901
+ refresh_method: refreshResult.method || null,
902
+ refresh_forced_recent_not_view: forceRecentNotView,
903
+ recovery_reason: reason
904
+ });
905
+ return refreshResult;
906
+ }
907
+
866
908
  runControl.setPhase("recommend:cleanup");
867
909
  await closeRecommendDetail(client, { attemptsLimit: 2 });
910
+ await closeRecommendBlockingPanelsForRun("cleanup");
868
911
 
869
912
  await runControl.waitIfPaused();
870
913
  runControl.throwIfCanceled();
871
914
  runControl.setPhase("recommend:roots");
872
- let rootState = await getRecommendRoots(client);
873
- rootState = await ensureRecommendViewport(rootState, "roots");
874
- runControl.checkpoint({
875
- iframe_selector: rootState.iframe.selector,
876
- iframe_document_node_id: rootState.iframe.documentNodeId
877
- });
878
-
879
- if (jobLabel) {
880
- await runControl.waitIfPaused();
881
- runControl.throwIfCanceled();
882
- runControl.setPhase("recommend:job");
883
- jobSelection = await selectRecommendJob(client, rootState.iframe.documentNodeId, {
884
- jobLabel,
885
- settleMs: cardTimeoutMs > 45000 ? 12000 : 6000
886
- });
887
- if (!jobSelection.selected) {
888
- throw new Error(`Requested recommend job was not selected: ${jobSelection.reason}`);
889
- }
890
- rootState = await getRecommendRoots(client);
891
- rootState = await ensureRecommendViewport(rootState, "job");
892
- runControl.checkpoint({
893
- job_selection: compactJobSelection(jobSelection)
894
- });
895
- }
896
-
897
- await runControl.waitIfPaused();
898
- runControl.throwIfCanceled();
899
- runControl.setPhase("recommend:page-scope");
900
- pageScopeSelection = await selectRecommendPageScope(client, rootState.iframe.documentNodeId, {
901
- pageScope: requestedPageScope,
902
- fallbackScope: normalizedFallbackPageScope,
903
- settleMs: cardTimeoutMs > 45000 ? 3000 : 1200,
904
- timeoutMs: Math.min(Math.max(cardTimeoutMs, 10000), 60000)
905
- });
906
- if (!pageScopeSelection.selected) {
907
- throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
908
- }
909
915
  rootState = await getRecommendRoots(client);
910
- rootState = await ensureRecommendViewport(rootState, "page_scope");
911
- runControl.checkpoint({
912
- page_scope: compactPageScopeSelection(pageScopeSelection)
913
- });
914
-
915
- if (normalizedFilter.enabled) {
916
- await runControl.waitIfPaused();
917
- runControl.throwIfCanceled();
918
- runControl.setPhase("recommend:filter");
919
- filterResult = await selectAndConfirmFirstSafeFilter(
920
- client,
921
- rootState.iframe.documentNodeId,
922
- buildRecommendFilterSelectionOptions(normalizedFilter)
923
- );
924
- if (!filterResult.confirmed) {
925
- throw new Error("Recommend run filter selection was not confirmed");
926
- }
927
- rootState = await getRecommendRoots(client);
928
- rootState = await ensureRecommendViewport(rootState, "filter");
929
- runControl.checkpoint({
930
- filter: compactFilterResult(filterResult)
931
- });
932
- }
933
-
934
- await runControl.waitIfPaused();
935
- runControl.throwIfCanceled();
936
- runControl.setPhase("recommend:cards");
937
- rootState = await ensureRecommendViewport(rootState, "cards");
938
- cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
939
- timeoutMs: cardTimeoutMs,
940
- intervalMs: 300
941
- });
942
- if (!cardNodeIds.length) {
943
- throw new Error("No recommend candidate cards found for run service");
944
- }
945
-
946
- updateRecommendProgress({
947
- list_end_reason: null
948
- });
949
-
950
- while (countPassedResults(results) < targetPassCount) {
951
- const candidateStarted = Date.now();
952
- const timings = {};
953
- await runControl.waitIfPaused();
954
- runControl.throwIfCanceled();
955
- runControl.setPhase("recommend:candidate");
956
- rootState = await ensureRecommendViewport(rootState, "candidate_loop");
957
-
958
- const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
959
- client,
960
- state: listState,
961
- maxScrolls: listMaxScrolls,
916
+ rootState = await ensureRecommendViewport(rootState, "roots");
917
+ runControl.checkpoint({
918
+ iframe_selector: rootState.iframe.selector,
919
+ iframe_document_node_id: rootState.iframe.documentNodeId
920
+ });
921
+
922
+ if (jobLabel) {
923
+ await runControl.waitIfPaused();
924
+ runControl.throwIfCanceled();
925
+ runControl.setPhase("recommend:job");
926
+ jobSelection = await selectRecommendJob(client, rootState.iframe.documentNodeId, {
927
+ jobLabel,
928
+ settleMs: cardTimeoutMs > 45000 ? 12000 : 6000
929
+ });
930
+ if (!jobSelection.selected) {
931
+ throw new Error(`Requested recommend job was not selected: ${jobSelection.reason}`);
932
+ }
933
+ rootState = await getRecommendRoots(client);
934
+ rootState = await ensureRecommendViewport(rootState, "job");
935
+ runControl.checkpoint({
936
+ job_selection: compactJobSelection(jobSelection)
937
+ });
938
+ }
939
+
940
+ await runControl.waitIfPaused();
941
+ runControl.throwIfCanceled();
942
+ runControl.setPhase("recommend:page-scope");
943
+ pageScopeSelection = await selectRecommendPageScope(client, rootState.iframe.documentNodeId, {
944
+ pageScope: requestedPageScope,
945
+ fallbackScope: normalizedFallbackPageScope,
946
+ settleMs: cardTimeoutMs > 45000 ? 3000 : 1200,
947
+ timeoutMs: Math.min(Math.max(cardTimeoutMs, 10000), 60000)
948
+ });
949
+ if (!pageScopeSelection.selected) {
950
+ throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
951
+ }
952
+ rootState = await getRecommendRoots(client);
953
+ rootState = await ensureRecommendViewport(rootState, "page_scope");
954
+ runControl.checkpoint({
955
+ page_scope: compactPageScopeSelection(pageScopeSelection)
956
+ });
957
+
958
+ if (normalizedFilter.enabled) {
959
+ await runControl.waitIfPaused();
960
+ runControl.throwIfCanceled();
961
+ runControl.setPhase("recommend:filter");
962
+ filterResult = await selectAndConfirmFirstSafeFilter(
963
+ client,
964
+ rootState.iframe.documentNodeId,
965
+ buildRecommendFilterSelectionOptions(normalizedFilter)
966
+ );
967
+ if (!filterResult.confirmed) {
968
+ throw new Error("Recommend run filter selection was not confirmed");
969
+ }
970
+ rootState = await getRecommendRoots(client);
971
+ rootState = await ensureRecommendViewport(rootState, "filter");
972
+ runControl.checkpoint({
973
+ filter: compactFilterResult(filterResult)
974
+ });
975
+ }
976
+
977
+ await runControl.waitIfPaused();
978
+ runControl.throwIfCanceled();
979
+ runControl.setPhase("recommend:cards");
980
+ rootState = await ensureRecommendViewport(rootState, "cards");
981
+ cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
982
+ timeoutMs: cardTimeoutMs,
983
+ intervalMs: 300
984
+ });
985
+ if (!cardNodeIds.length) {
986
+ throw new Error("No recommend candidate cards found for run service");
987
+ }
988
+
989
+ updateRecommendProgress({
990
+ list_end_reason: null
991
+ });
992
+
993
+ while (countPassedResults(results) < targetPassCount) {
994
+ const candidateStarted = Date.now();
995
+ const timings = {};
996
+ await runControl.waitIfPaused();
997
+ runControl.throwIfCanceled();
998
+ runControl.setPhase("recommend:candidate");
999
+ rootState = await ensureRecommendViewport(rootState, "candidate_loop");
1000
+
1001
+ const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
1002
+ client,
1003
+ state: listState,
1004
+ maxScrolls: listMaxScrolls,
962
1005
  stableSignatureLimit: listStableSignatureLimit,
963
1006
  wheelDeltaY: listWheelDeltaY,
964
1007
  settleMs: listSettleMs,
965
1008
  listScrollJitterEnabled: effectiveHumanBehavior.listScrollJitter,
966
1009
  fallbackPoint: listFallbackResolver,
967
- findNodeIds: async () => {
968
- let currentRootState = await getRecommendRoots(client);
969
- currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
970
- rootState = currentRootState;
971
- const currentCardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
972
- timeoutMs: Math.min(cardTimeoutMs, 5000),
973
- intervalMs: 300
974
- });
975
- cardNodeIds = currentCardNodeIds;
976
- return currentCardNodeIds;
977
- },
978
- readCandidate: async (nodeId, { visibleIndex }) => readRecommendCardCandidate(client, nodeId, {
979
- targetUrl,
980
- source: "recommend-run-card",
981
- metadata: {
982
- run_candidate_index: results.length,
983
- visible_index: visibleIndex
984
- }
985
- }),
986
- detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
987
- rootNodeId: rootState?.iframe?.documentNodeId,
988
- markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
989
- refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
990
- textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
991
- maxTextScanNodes: 500
992
- })
993
- }));
994
- if (!nextCandidateResult.ok) {
995
- listEndReason = nextCandidateResult.reason || "list_exhausted";
996
- if (
997
- (nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
998
- && refreshOnEnd
999
- && countPassedResults(results) < targetPassCount
1000
- && refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
1001
- ) {
1002
- await runControl.waitIfPaused();
1003
- runControl.throwIfCanceled();
1004
- runControl.setPhase("recommend:refresh");
1005
- refreshRounds += 1;
1006
- const refreshResult = await refreshRecommendListAtEnd(client, {
1007
- rootState,
1008
- jobLabel,
1009
- pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
1010
- fallbackPageScope: normalizedFallbackPageScope,
1011
- filter: normalizedFilter,
1012
- forceRecentNotView: true,
1013
- cardTimeoutMs,
1014
- buttonSettleMs: refreshButtonSettleMs,
1015
- reloadSettleMs: refreshReloadSettleMs
1016
- });
1017
- const compactRefresh = compactRefreshAttempt(refreshResult);
1018
- refreshAttempts.push(compactRefresh);
1019
- runControl.checkpoint({
1020
- refresh_round: refreshRounds,
1021
- refresh: compactRefresh
1022
- });
1023
- updateRecommendProgress({
1024
- card_count: refreshResult.card_count || cardNodeIds.length,
1025
- refresh_method: refreshResult.method || null,
1026
- refresh_forced_recent_not_view: true,
1027
- list_end_reason: listEndReason
1028
- });
1010
+ findNodeIds: async () => {
1011
+ let currentRootState = await getRecommendRoots(client);
1012
+ currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
1013
+ rootState = currentRootState;
1014
+ const currentCardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
1015
+ timeoutMs: Math.min(cardTimeoutMs, 5000),
1016
+ intervalMs: 300
1017
+ });
1018
+ cardNodeIds = currentCardNodeIds;
1019
+ return currentCardNodeIds;
1020
+ },
1021
+ readCandidate: async (nodeId, { visibleIndex }) => readRecommendCardCandidate(client, nodeId, {
1022
+ targetUrl,
1023
+ source: "recommend-run-card",
1024
+ metadata: {
1025
+ run_candidate_index: results.length,
1026
+ visible_index: visibleIndex
1027
+ }
1028
+ }),
1029
+ detectBottomMarker: async ({ scrollAttempt = 0, signature = {} } = {}) => detectInfiniteListBottomMarker(client, {
1030
+ rootNodeId: rootState?.iframe?.documentNodeId,
1031
+ markerSelectors: RECOMMEND_BOTTOM_MARKER_SELECTORS,
1032
+ refreshSelectors: [RECOMMEND_END_REFRESH_SELECTOR],
1033
+ textScanSelectors: scrollAttempt > 0 || (signature?.stable_signature_count || 0) >= 2 ? undefined : [],
1034
+ maxTextScanNodes: 500
1035
+ })
1036
+ }));
1037
+ if (!nextCandidateResult.ok) {
1038
+ listEndReason = nextCandidateResult.reason || "list_exhausted";
1039
+ if (
1040
+ (nextCandidateResult.end_reached || isRefreshableListStall(nextCandidateResult.reason))
1041
+ && refreshOnEnd
1042
+ && countPassedResults(results) < targetPassCount
1043
+ && refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
1044
+ ) {
1045
+ await runControl.waitIfPaused();
1046
+ runControl.throwIfCanceled();
1047
+ runControl.setPhase("recommend:refresh");
1048
+ refreshRounds += 1;
1049
+ const refreshResult = await refreshRecommendListAtEnd(client, {
1050
+ rootState,
1051
+ jobLabel,
1052
+ pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
1053
+ fallbackPageScope: normalizedFallbackPageScope,
1054
+ filter: normalizedFilter,
1055
+ forceRecentNotView: true,
1056
+ cardTimeoutMs,
1057
+ buttonSettleMs: refreshButtonSettleMs,
1058
+ reloadSettleMs: refreshReloadSettleMs
1059
+ });
1060
+ const compactRefresh = compactRefreshAttempt(refreshResult);
1061
+ refreshAttempts.push(compactRefresh);
1062
+ runControl.checkpoint({
1063
+ refresh_round: refreshRounds,
1064
+ refresh: compactRefresh
1065
+ });
1066
+ updateRecommendProgress({
1067
+ card_count: refreshResult.card_count || cardNodeIds.length,
1068
+ refresh_method: refreshResult.method || null,
1069
+ refresh_forced_recent_not_view: true,
1070
+ list_end_reason: listEndReason
1071
+ });
1029
1072
  if (refreshResult.ok) {
1030
1073
  rootState = refreshResult.root_state || await getRecommendRoots(client);
1031
1074
  rootState = await ensureRecommendViewport(rootState, "refresh_after");
1032
- cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1033
- timeoutMs: cardTimeoutMs,
1034
- intervalMs: 300
1035
- });
1036
- resetInfiniteListForRefreshRound(listState, {
1037
- reason: listEndReason,
1038
- round: refreshRounds,
1039
- method: refreshResult.method,
1040
- metadata: {
1041
- card_count: cardNodeIds.length,
1042
- forced_recent_not_view: true
1043
- }
1044
- });
1075
+ cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
1076
+ timeoutMs: cardTimeoutMs,
1077
+ intervalMs: 300
1078
+ });
1079
+ resetInfiniteListForRefreshRound(listState, {
1080
+ reason: listEndReason,
1081
+ round: refreshRounds,
1082
+ method: refreshResult.method,
1083
+ metadata: {
1084
+ card_count: cardNodeIds.length,
1085
+ forced_recent_not_view: true
1086
+ }
1087
+ });
1045
1088
  listEndReason = "";
1046
1089
  continue;
1047
1090
  }
@@ -1053,240 +1096,262 @@ export async function runRecommendWorkflow({
1053
1096
  }
1054
1097
  break;
1055
1098
  }
1056
-
1057
- const index = results.length;
1058
- let cardNodeId = nextCandidateResult.item.node_id;
1059
- const candidateKey = nextCandidateResult.item.key;
1060
- let cardCandidate = nextCandidateResult.item.candidate;
1061
-
1062
- let screeningCandidate = cardCandidate;
1063
- let detailResult = null;
1064
- let recoverableDetailError = null;
1065
- let detailStep = "not_started";
1066
- if (index < effectiveDetailLimit) {
1067
- try {
1068
- await runControl.waitIfPaused();
1069
- runControl.throwIfCanceled();
1099
+
1100
+ const index = results.length;
1101
+ let cardNodeId = nextCandidateResult.item.node_id;
1102
+ const candidateKey = nextCandidateResult.item.key;
1103
+ let cardCandidate = nextCandidateResult.item.candidate;
1104
+
1105
+ let screeningCandidate = cardCandidate;
1106
+ let detailResult = null;
1107
+ let recoverableDetailError = null;
1108
+ let detailStep = "not_started";
1109
+ if (index < effectiveDetailLimit) {
1110
+ try {
1111
+ await runControl.waitIfPaused();
1112
+ runControl.throwIfCanceled();
1070
1113
  runControl.setPhase("recommend:detail");
1071
1114
  detailStep = "ensure_viewport";
1072
1115
  rootState = await ensureRecommendViewport(rootState, "detail");
1116
+ const blockingPanelClose = await closeRecommendBlockingPanels(client, {
1117
+ attemptsLimit: 2,
1118
+ rootState
1119
+ });
1120
+ if (!blockingPanelClose?.closed) {
1121
+ const panelError = createRecommendBlockingPanelCloseFailureError(
1122
+ blockingPanelClose,
1123
+ "before_detail_open"
1124
+ );
1125
+ timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1126
+ checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
1127
+ await recoverAndReapplyRecommendContext("account_rights_panel_before_detail", panelError, {
1128
+ forceRecentNotView: true
1129
+ });
1130
+ continue;
1131
+ }
1132
+ if (blockingPanelClose.already_closed === false) {
1133
+ timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
1134
+ }
1073
1135
  checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
1074
1136
  detailStep = "open_detail";
1075
1137
  networkRecorder.clear();
1076
1138
  await maybeHumanActionCooldown("before_detail_open", timings);
1077
1139
  const openedDetail = await openRecommendCardDetailWithFreshRetry(client, {
1078
- cardNodeId,
1079
- candidateKey,
1080
- cardCandidate,
1081
- rootState,
1082
- targetUrl,
1083
- retryTimeoutMs: 8000,
1084
- maxAttempts: 3
1085
- });
1086
- addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
1087
- addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
1088
- cardNodeId = openedDetail.card_node_id || cardNodeId;
1089
- cardCandidate = openedDetail.card_candidate || cardCandidate;
1090
- screeningCandidate = cardCandidate;
1091
- const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1092
- detailStep = "wait_network";
1093
- const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1094
- waitForRecommendDetailNetworkEvents,
1095
- networkRecorder,
1096
- {
1097
- waitPlan,
1098
- minCount: 1,
1099
- requireLoaded: true,
1100
- intervalMs: 120
1101
- }
1102
- ));
1103
- if (networkWait?.elapsed_ms != null) {
1104
- timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
1105
- }
1106
- detailStep = "extract_detail";
1107
- detailResult = await extractRecommendDetailCandidate(client, {
1108
- cardCandidate,
1109
- cardNodeId,
1110
- detailState: openedDetail.detail_state,
1111
- networkEvents: networkRecorder.events,
1112
- targetUrl,
1113
- closeDetail: false,
1114
- networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1115
- networkParseIntervalMs: 250
1116
- });
1117
- addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1118
-
1119
- const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1120
- let source = "network";
1121
- let imageEvidence = null;
1122
- let captureTarget = null;
1123
- let captureTargetWait = null;
1124
- if (parsedNetworkProfileCount > 0) {
1125
- recordCvNetworkHit(cvAcquisitionState, {
1126
- parsedNetworkProfileCount,
1127
- waitResult: networkWait
1128
- });
1129
- } else {
1130
- detailStep = "wait_capture_target";
1131
- captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
1132
- domain: "recommend",
1133
- timeoutMs: 6000,
1134
- intervalMs: 250
1135
- });
1136
- captureTarget = captureTargetWait.target || null;
1137
- const captureNodeId = captureTarget?.node_id || null;
1138
- if (captureNodeId) {
1139
- const imageEvidencePath = imageEvidenceFilePath({
1140
- imageOutputDir,
1141
- domain: "recommend",
1142
- runId: runControl?.runId,
1143
- index,
1144
- extension: "jpg"
1145
- });
1146
- try {
1147
- detailStep = "capture_image";
1148
- imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
1149
- filePath: imageEvidencePath,
1150
- format: "jpeg",
1151
- quality: 72,
1152
- optimize: true,
1153
- resizeMaxWidth: 1100,
1154
- captureViewport: false,
1155
- padding: 0,
1140
+ cardNodeId,
1141
+ candidateKey,
1142
+ cardCandidate,
1143
+ rootState,
1144
+ targetUrl,
1145
+ retryTimeoutMs: 8000,
1146
+ maxAttempts: 3
1147
+ });
1148
+ addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
1149
+ addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
1150
+ cardNodeId = openedDetail.card_node_id || cardNodeId;
1151
+ cardCandidate = openedDetail.card_candidate || cardCandidate;
1152
+ screeningCandidate = cardCandidate;
1153
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
1154
+ detailStep = "wait_network";
1155
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
1156
+ waitForRecommendDetailNetworkEvents,
1157
+ networkRecorder,
1158
+ {
1159
+ waitPlan,
1160
+ minCount: 1,
1161
+ requireLoaded: true,
1162
+ intervalMs: 120
1163
+ }
1164
+ ));
1165
+ if (networkWait?.elapsed_ms != null) {
1166
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
1167
+ }
1168
+ detailStep = "extract_detail";
1169
+ detailResult = await extractRecommendDetailCandidate(client, {
1170
+ cardCandidate,
1171
+ cardNodeId,
1172
+ detailState: openedDetail.detail_state,
1173
+ networkEvents: networkRecorder.events,
1174
+ targetUrl,
1175
+ closeDetail: false,
1176
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
1177
+ networkParseIntervalMs: 250
1178
+ });
1179
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
1180
+
1181
+ const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
1182
+ let source = "network";
1183
+ let imageEvidence = null;
1184
+ let captureTarget = null;
1185
+ let captureTargetWait = null;
1186
+ if (parsedNetworkProfileCount > 0) {
1187
+ recordCvNetworkHit(cvAcquisitionState, {
1188
+ parsedNetworkProfileCount,
1189
+ waitResult: networkWait
1190
+ });
1191
+ } else {
1192
+ detailStep = "wait_capture_target";
1193
+ captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
1194
+ domain: "recommend",
1195
+ timeoutMs: 6000,
1196
+ intervalMs: 250
1197
+ });
1198
+ captureTarget = captureTargetWait.target || null;
1199
+ const captureNodeId = captureTarget?.node_id || null;
1200
+ if (captureNodeId) {
1201
+ const imageEvidencePath = imageEvidenceFilePath({
1202
+ imageOutputDir,
1203
+ domain: "recommend",
1204
+ runId: runControl?.runId,
1205
+ index,
1206
+ extension: "jpg"
1207
+ });
1208
+ try {
1209
+ detailStep = "capture_image";
1210
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
1211
+ filePath: imageEvidencePath,
1212
+ format: "jpeg",
1213
+ quality: 72,
1214
+ optimize: true,
1215
+ resizeMaxWidth: 1100,
1216
+ captureViewport: false,
1217
+ padding: 0,
1156
1218
  maxScreenshots: maxImagePages,
1157
1219
  wheelDeltaY: imageWheelDeltaY,
1158
1220
  settleMs: 350,
1159
1221
  scrollMethod: "dom-anchor-fallback-input",
1160
1222
  scrollDeltaJitterEnabled: effectiveHumanBehavior.listScrollJitter,
1161
1223
  stepTimeoutMs: 45000,
1162
- totalTimeoutMs: 90000,
1163
- duplicateStopCount: 1,
1164
- skipDuplicateScreenshots: true,
1165
- composeForLlm: true,
1166
- llmPagesPerImage: 3,
1167
- llmResizeMaxWidth: 1100,
1168
- llmQuality: 72,
1169
- metadata: {
1170
- domain: "recommend",
1171
- capture_mode: "scroll_sequence",
1172
- acquisition_reason: "network_miss_image_fallback",
1173
- run_candidate_index: index,
1174
- candidate_key: candidateKey,
1175
- capture_target: captureTarget,
1176
- capture_target_wait: captureTargetWait
1177
- }
1178
- }));
1179
- source = "image";
1180
- } catch (error) {
1181
- if (!isRecoverableImageCaptureError(error)) throw error;
1182
- const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1183
- if (recoveryCount < 1) {
1184
- candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
1224
+ totalTimeoutMs: 90000,
1225
+ duplicateStopCount: 1,
1226
+ skipDuplicateScreenshots: true,
1227
+ composeForLlm: true,
1228
+ llmPagesPerImage: 3,
1229
+ llmResizeMaxWidth: 1100,
1230
+ llmQuality: 72,
1231
+ metadata: {
1232
+ domain: "recommend",
1233
+ capture_mode: "scroll_sequence",
1234
+ acquisition_reason: "network_miss_image_fallback",
1235
+ run_candidate_index: index,
1236
+ candidate_key: candidateKey,
1237
+ capture_target: captureTarget,
1238
+ capture_target_wait: captureTargetWait
1239
+ }
1240
+ }));
1241
+ source = "image";
1242
+ } catch (error) {
1243
+ if (!isRecoverableImageCaptureError(error)) throw error;
1244
+ const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1245
+ if (recoveryCount < 1) {
1246
+ candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
1185
1247
  timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
1186
1248
  checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1187
1249
  await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1250
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1188
1251
  await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
1189
1252
  forceRecentNotView: true
1190
1253
  });
1191
- continue;
1192
- }
1193
- imageEvidence = createRecoverableImageCaptureEvidence(error, {
1194
- elapsedMs: timings.screenshot_capture_ms,
1195
- filePath: imageEvidencePath,
1196
- extension: "jpg",
1197
- maxScreenshots: maxImagePages
1198
- });
1199
- source = "image_capture_failed";
1200
- }
1201
- recordCvImageFallback(cvAcquisitionState, {
1202
- reason: source === "image_capture_failed"
1203
- ? "network_miss_image_capture_failed"
1204
- : "network_miss_image_fallback",
1205
- parsedNetworkProfileCount,
1206
- waitResult: networkWait,
1207
- imageEvidence
1208
- });
1209
- } else {
1210
- source = "missing_capture_node";
1211
- recordCvNetworkMiss(cvAcquisitionState, {
1212
- reason: "network_miss_no_capture_node",
1213
- parsedNetworkProfileCount,
1214
- waitResult: networkWait
1215
- });
1216
- }
1217
- }
1218
-
1219
- detailResult.image_evidence = imageEvidence;
1220
- detailResult.cv_acquisition = {
1221
- source,
1222
- mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
1223
- wait_plan: waitPlan,
1224
- network_wait: networkWait,
1225
- parsed_network_profile_count: parsedNetworkProfileCount,
1226
- image_evidence: summarizeImageEvidence(imageEvidence),
1227
- capture_target: captureTarget || null,
1228
- capture_target_wait: captureTargetWait
1229
- };
1230
- screeningCandidate = detailResult.candidate;
1231
- } catch (error) {
1232
- if (!isRecoverableRecommendDetailError(error)) throw error;
1233
- const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1234
- if (recoveryCount < 1) {
1254
+ continue;
1255
+ }
1256
+ imageEvidence = createRecoverableImageCaptureEvidence(error, {
1257
+ elapsedMs: timings.screenshot_capture_ms,
1258
+ filePath: imageEvidencePath,
1259
+ extension: "jpg",
1260
+ maxScreenshots: maxImagePages
1261
+ });
1262
+ source = "image_capture_failed";
1263
+ }
1264
+ recordCvImageFallback(cvAcquisitionState, {
1265
+ reason: source === "image_capture_failed"
1266
+ ? "network_miss_image_capture_failed"
1267
+ : "network_miss_image_fallback",
1268
+ parsedNetworkProfileCount,
1269
+ waitResult: networkWait,
1270
+ imageEvidence
1271
+ });
1272
+ } else {
1273
+ source = "missing_capture_node";
1274
+ recordCvNetworkMiss(cvAcquisitionState, {
1275
+ reason: "network_miss_no_capture_node",
1276
+ parsedNetworkProfileCount,
1277
+ waitResult: networkWait
1278
+ });
1279
+ }
1280
+ }
1281
+
1282
+ detailResult.image_evidence = imageEvidence;
1283
+ detailResult.cv_acquisition = {
1284
+ source,
1285
+ mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
1286
+ wait_plan: waitPlan,
1287
+ network_wait: networkWait,
1288
+ parsed_network_profile_count: parsedNetworkProfileCount,
1289
+ image_evidence: summarizeImageEvidence(imageEvidence),
1290
+ capture_target: captureTarget || null,
1291
+ capture_target_wait: captureTargetWait
1292
+ };
1293
+ screeningCandidate = detailResult.candidate;
1294
+ } catch (error) {
1295
+ if (!isRecoverableRecommendDetailError(error)) throw error;
1296
+ const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
1297
+ if (recoveryCount < 1) {
1235
1298
  candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
1236
1299
  timings.detail_recovery_trigger = compactRecoverableDetailError(error);
1237
1300
  checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1238
1301
  await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1302
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1239
1303
  await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
1240
1304
  forceRecentNotView: true
1241
1305
  });
1242
- continue;
1243
- }
1306
+ continue;
1307
+ }
1244
1308
  recoverableDetailError = error;
1245
1309
  detailResult = null;
1246
1310
  timings.detail_recovered_error = compactRecoverableDetailError(error);
1247
1311
  await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1312
+ await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1248
1313
  }
1249
- }
1250
-
1251
- await runControl.waitIfPaused();
1252
- runControl.throwIfCanceled();
1253
- runControl.setPhase("recommend:screening");
1254
- let llmResult = null;
1255
- if (useLlmScreening) {
1256
- if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
1257
- llmResult = null;
1258
- } else if (!llmConfig) {
1259
- llmResult = createMissingLlmConfigResult();
1260
- } else {
1261
- try {
1262
- const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
1263
- ? "vision_model_ms"
1264
- : "text_model_ms";
1265
- llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1266
- candidate: screeningCandidate,
1267
- criteria,
1268
- config: llmConfig,
1269
- timeoutMs: llmTimeoutMs,
1270
- imageEvidence: detailResult?.image_evidence || null,
1271
- maxImages: llmImageLimit,
1272
- imageDetail: llmImageDetail
1273
- }));
1274
- } catch (error) {
1275
- llmResult = createFailedLlmScreeningResult(error);
1276
- }
1277
- }
1278
- if (detailResult) detailResult.llm_result = llmResult;
1279
- }
1280
- const screening = recoverableDetailError
1281
- ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
1282
- : detailResult?.image_evidence?.ok === false
1283
- ? createImageCaptureFailureScreening(screeningCandidate, {
1284
- code: detailResult.image_evidence.error_code,
1285
- message: detailResult.image_evidence.error
1286
- })
1287
- : useLlmScreening
1288
- ? llmResultToScreening(llmResult, screeningCandidate)
1289
- : screenCandidate(screeningCandidate, { criteria });
1314
+ }
1315
+
1316
+ await runControl.waitIfPaused();
1317
+ runControl.throwIfCanceled();
1318
+ runControl.setPhase("recommend:screening");
1319
+ let llmResult = null;
1320
+ if (useLlmScreening) {
1321
+ if (recoverableDetailError || detailResult?.image_evidence?.ok === false) {
1322
+ llmResult = null;
1323
+ } else if (!llmConfig) {
1324
+ llmResult = createMissingLlmConfigResult();
1325
+ } else {
1326
+ try {
1327
+ const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
1328
+ ? "vision_model_ms"
1329
+ : "text_model_ms";
1330
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
1331
+ candidate: screeningCandidate,
1332
+ criteria,
1333
+ config: llmConfig,
1334
+ timeoutMs: llmTimeoutMs,
1335
+ imageEvidence: detailResult?.image_evidence || null,
1336
+ maxImages: llmImageLimit,
1337
+ imageDetail: llmImageDetail
1338
+ }));
1339
+ } catch (error) {
1340
+ llmResult = createFailedLlmScreeningResult(error);
1341
+ }
1342
+ }
1343
+ if (detailResult) detailResult.llm_result = llmResult;
1344
+ }
1345
+ const screening = recoverableDetailError
1346
+ ? createRecoverableDetailFailureScreening(screeningCandidate, recoverableDetailError)
1347
+ : detailResult?.image_evidence?.ok === false
1348
+ ? createImageCaptureFailureScreening(screeningCandidate, {
1349
+ code: detailResult.image_evidence.error_code,
1350
+ message: detailResult.image_evidence.error
1351
+ })
1352
+ : useLlmScreening
1353
+ ? llmResultToScreening(llmResult, screeningCandidate)
1354
+ : screenCandidate(screeningCandidate, { criteria });
1290
1355
  let actionDiscovery = null;
1291
1356
  let postActionResult = null;
1292
1357
  let closeFailureError = null;
@@ -1298,24 +1363,24 @@ export async function runRecommendWorkflow({
1298
1363
  runControl.setPhase("recommend:post-action");
1299
1364
  await maybeHumanActionCooldown("before_post_action", timings);
1300
1365
  actionDiscovery = await waitForRecommendDetailActionControls(client, {
1301
- timeoutMs: actionTimeoutMs,
1302
- intervalMs: actionIntervalMs,
1303
- requireAny: true
1304
- });
1305
- postActionResult = await runRecommendPostAction({
1306
- client,
1307
- screening,
1308
- actionDiscovery,
1309
- postAction: normalizedPostAction,
1310
- greetCount,
1311
- maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1312
- executePostAction,
1313
- afterClickDelayMs: actionAfterClickDelayMs
1314
- });
1315
- if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
1316
- greetCount += 1;
1317
- }
1318
- addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1366
+ timeoutMs: actionTimeoutMs,
1367
+ intervalMs: actionIntervalMs,
1368
+ requireAny: true
1369
+ });
1370
+ postActionResult = await runRecommendPostAction({
1371
+ client,
1372
+ screening,
1373
+ actionDiscovery,
1374
+ postAction: normalizedPostAction,
1375
+ greetCount,
1376
+ maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1377
+ executePostAction,
1378
+ afterClickDelayMs: actionAfterClickDelayMs
1379
+ });
1380
+ if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
1381
+ greetCount += 1;
1382
+ }
1383
+ addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
1319
1384
  }
1320
1385
  if (detailResult && closeDetail) {
1321
1386
  detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
@@ -1349,16 +1414,16 @@ export async function runRecommendWorkflow({
1349
1414
  }
1350
1415
  }
1351
1416
  }
1352
- timings.total_ms = Date.now() - candidateStarted;
1353
- const compactResult = {
1354
- index,
1355
- candidate_key: candidateKey,
1356
- card_node_id: cardNodeId,
1357
- candidate: compactCandidate(screeningCandidate),
1358
- detail: compactDetail(detailResult),
1359
- llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
1360
- screening: compactScreening(screening),
1361
- action_discovery: compactActionDiscovery(actionDiscovery),
1417
+ timings.total_ms = Date.now() - candidateStarted;
1418
+ const compactResult = {
1419
+ index,
1420
+ candidate_key: candidateKey,
1421
+ card_node_id: cardNodeId,
1422
+ candidate: compactCandidate(screeningCandidate),
1423
+ detail: compactDetail(detailResult),
1424
+ llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
1425
+ screening: compactScreening(screening),
1426
+ action_discovery: compactActionDiscovery(actionDiscovery),
1362
1427
  post_action: postActionResult,
1363
1428
  error: recoverableDetailError
1364
1429
  ? compactRecoverableDetailError(recoverableDetailError)
@@ -1367,40 +1432,40 @@ export async function runRecommendWorkflow({
1367
1432
  : detailResult?.image_evidence?.ok === false
1368
1433
  ? compactError({
1369
1434
  code: detailResult.image_evidence.error_code,
1370
- message: detailResult.image_evidence.error
1371
- }, "IMAGE_CAPTURE_FAILED")
1372
- : null,
1373
- timings
1374
- };
1375
- results.push(compactResult);
1376
- markInfiniteListCandidateProcessed(listState, candidateKey, {
1377
- metadata: {
1378
- result_index: index,
1379
- candidate_id: screeningCandidate.id || null
1380
- }
1381
- });
1382
-
1383
- updateRecommendProgress({
1384
- last_candidate_id: screeningCandidate.id || null,
1385
- last_candidate_key: candidateKey,
1386
- last_score: screening.score
1387
- });
1388
- const checkpointStarted = Date.now();
1389
- runControl.checkpoint({
1390
- results,
1391
- last_candidate: {
1392
- id: screeningCandidate.id || null,
1393
- key: candidateKey,
1394
- identity: screeningCandidate.identity || {},
1395
- screening: {
1396
- status: screening.status,
1397
- passed: screening.passed,
1398
- score: screening.score
1399
- },
1400
- llm_screening: compactScreeningLlmResult(llmResult),
1401
- error: compactResult.error,
1402
- post_action: postActionResult
1403
- }
1435
+ message: detailResult.image_evidence.error
1436
+ }, "IMAGE_CAPTURE_FAILED")
1437
+ : null,
1438
+ timings
1439
+ };
1440
+ results.push(compactResult);
1441
+ markInfiniteListCandidateProcessed(listState, candidateKey, {
1442
+ metadata: {
1443
+ result_index: index,
1444
+ candidate_id: screeningCandidate.id || null
1445
+ }
1446
+ });
1447
+
1448
+ updateRecommendProgress({
1449
+ last_candidate_id: screeningCandidate.id || null,
1450
+ last_candidate_key: candidateKey,
1451
+ last_score: screening.score
1452
+ });
1453
+ const checkpointStarted = Date.now();
1454
+ runControl.checkpoint({
1455
+ results,
1456
+ last_candidate: {
1457
+ id: screeningCandidate.id || null,
1458
+ key: candidateKey,
1459
+ identity: screeningCandidate.identity || {},
1460
+ screening: {
1461
+ status: screening.status,
1462
+ passed: screening.passed,
1463
+ score: screening.score
1464
+ },
1465
+ llm_screening: compactScreeningLlmResult(llmResult),
1466
+ error: compactResult.error,
1467
+ post_action: postActionResult
1468
+ }
1404
1469
  });
1405
1470
  addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
1406
1471
 
@@ -1439,18 +1504,18 @@ export async function runRecommendWorkflow({
1439
1504
  await runControl.sleep(delayMs);
1440
1505
  addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
1441
1506
  compactResult.timings.total_ms = Date.now() - candidateStarted;
1442
- }
1443
- }
1444
-
1445
- runControl.setPhase("recommend:done");
1446
- return {
1447
- domain: "recommend",
1448
- target_url: targetUrl,
1449
- job_selection: compactJobSelection(jobSelection),
1450
- page_scope: compactPageScopeSelection(pageScopeSelection),
1451
- filter: compactFilterResult(filterResult),
1452
- card_count: cardNodeIds.length,
1453
- candidate_list: compactInfiniteListState(listState),
1507
+ }
1508
+ }
1509
+
1510
+ runControl.setPhase("recommend:done");
1511
+ return {
1512
+ domain: "recommend",
1513
+ target_url: targetUrl,
1514
+ job_selection: compactJobSelection(jobSelection),
1515
+ page_scope: compactPageScopeSelection(pageScopeSelection),
1516
+ filter: compactFilterResult(filterResult),
1517
+ card_count: cardNodeIds.length,
1518
+ candidate_list: compactInfiniteListState(listState),
1454
1519
  viewport_health: {
1455
1520
  stats: viewportGuard.getStats(),
1456
1521
  events: viewportGuard.getEvents()
@@ -1459,56 +1524,56 @@ export async function runRecommendWorkflow({
1459
1524
  human_rest: humanRestController.getState(),
1460
1525
  last_human_event: lastHumanEvent,
1461
1526
  list_end_reason: listEndReason || null,
1462
- refresh_rounds: refreshRounds,
1463
- refresh_attempts: refreshAttempts,
1464
- context_recoveries: contextRecoveryAttempts,
1465
- ...countRecommendResultStatuses(results, { greetCount }),
1466
- results
1467
- };
1468
- }
1469
-
1470
- export function createRecommendRunService({
1471
- lifecycle,
1472
- idPrefix = "recommend",
1473
- workflow = runRecommendWorkflow,
1474
- onSnapshot = null
1475
- } = {}) {
1476
- const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1477
-
1478
- function startRecommendRun({
1479
- runId = "",
1480
- pid = process.pid,
1481
- client,
1482
- targetUrl = "",
1483
- criteria = "",
1484
- jobLabel = "",
1485
- pageScope = "recommend",
1486
- fallbackPageScope = "recommend",
1487
- filter = {},
1488
- maxCandidates = 5,
1489
- detailLimit,
1490
- closeDetail = true,
1491
- delayMs = 0,
1492
- cardTimeoutMs = 10000,
1493
- maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
1494
- imageWheelDeltaY = 650,
1495
- cvAcquisitionMode = "unknown",
1496
- listMaxScrolls = 20,
1497
- listStableSignatureLimit = 5,
1498
- listWheelDeltaY = 850,
1499
- listSettleMs = 2200,
1500
- listFallbackPoint = null,
1501
- refreshOnEnd = true,
1502
- maxRefreshRounds = 2,
1503
- refreshButtonSettleMs = 8000,
1504
- refreshReloadSettleMs = 8000,
1505
- postAction = "none",
1506
- maxGreetCount = null,
1507
- executePostAction = true,
1508
- actionTimeoutMs = 8000,
1509
- actionIntervalMs = 500,
1510
- actionAfterClickDelayMs = 900,
1511
- screeningMode = "llm",
1527
+ refresh_rounds: refreshRounds,
1528
+ refresh_attempts: refreshAttempts,
1529
+ context_recoveries: contextRecoveryAttempts,
1530
+ ...countRecommendResultStatuses(results, { greetCount }),
1531
+ results
1532
+ };
1533
+ }
1534
+
1535
+ export function createRecommendRunService({
1536
+ lifecycle,
1537
+ idPrefix = "recommend",
1538
+ workflow = runRecommendWorkflow,
1539
+ onSnapshot = null
1540
+ } = {}) {
1541
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1542
+
1543
+ function startRecommendRun({
1544
+ runId = "",
1545
+ pid = process.pid,
1546
+ client,
1547
+ targetUrl = "",
1548
+ criteria = "",
1549
+ jobLabel = "",
1550
+ pageScope = "recommend",
1551
+ fallbackPageScope = "recommend",
1552
+ filter = {},
1553
+ maxCandidates = 5,
1554
+ detailLimit,
1555
+ closeDetail = true,
1556
+ delayMs = 0,
1557
+ cardTimeoutMs = 10000,
1558
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
1559
+ imageWheelDeltaY = 650,
1560
+ cvAcquisitionMode = "unknown",
1561
+ listMaxScrolls = 20,
1562
+ listStableSignatureLimit = 5,
1563
+ listWheelDeltaY = 850,
1564
+ listSettleMs = 2200,
1565
+ listFallbackPoint = null,
1566
+ refreshOnEnd = true,
1567
+ maxRefreshRounds = 2,
1568
+ refreshButtonSettleMs = 8000,
1569
+ refreshReloadSettleMs = 8000,
1570
+ postAction = "none",
1571
+ maxGreetCount = null,
1572
+ executePostAction = true,
1573
+ actionTimeoutMs = 8000,
1574
+ actionIntervalMs = 500,
1575
+ actionAfterClickDelayMs = 900,
1576
+ screeningMode = "llm",
1512
1577
  llmConfig = null,
1513
1578
  llmTimeoutMs = 120000,
1514
1579
  llmImageLimit = 8,
@@ -1529,41 +1594,41 @@ export function createRecommendRunService({
1529
1594
  });
1530
1595
  const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1531
1596
  const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
1532
- const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
1533
- return manager.startRun({
1534
- runId,
1535
- name,
1536
- pid,
1537
- context: {
1538
- domain: "recommend",
1539
- target_url: targetUrl,
1540
- criteria_present: Boolean(criteria),
1541
- job_label: jobLabel || "",
1542
- requested_page_scope: requestedPageScope,
1543
- fallback_page_scope: normalizedFallbackPageScope,
1544
- filter: normalizedFilter,
1545
- max_candidates: maxCandidates,
1546
- max_candidates_semantics: "passed_candidates",
1547
- detail_limit: normalizedDetailLimit,
1548
- close_detail: closeDetail,
1549
- cv_acquisition_mode: cvAcquisitionMode,
1550
- max_image_pages: maxImagePages,
1551
- image_wheel_delta_y: imageWheelDeltaY,
1552
- list_max_scrolls: listMaxScrolls,
1553
- list_stable_signature_limit: listStableSignatureLimit,
1554
- list_wheel_delta_y: listWheelDeltaY,
1555
- list_settle_ms: listSettleMs,
1556
- list_fallback_point: listFallbackPoint,
1557
- refresh_on_end: refreshOnEnd,
1558
- max_refresh_rounds: maxRefreshRounds,
1559
- refresh_button_settle_ms: refreshButtonSettleMs,
1560
- refresh_reload_settle_ms: refreshReloadSettleMs,
1561
- post_action: normalizedPostAction,
1562
- max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1563
- execute_post_action: Boolean(executePostAction),
1564
- action_timeout_ms: actionTimeoutMs,
1565
- screening_mode: normalizedScreeningMode,
1566
- llm_configured: Boolean(llmConfig),
1597
+ const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
1598
+ return manager.startRun({
1599
+ runId,
1600
+ name,
1601
+ pid,
1602
+ context: {
1603
+ domain: "recommend",
1604
+ target_url: targetUrl,
1605
+ criteria_present: Boolean(criteria),
1606
+ job_label: jobLabel || "",
1607
+ requested_page_scope: requestedPageScope,
1608
+ fallback_page_scope: normalizedFallbackPageScope,
1609
+ filter: normalizedFilter,
1610
+ max_candidates: maxCandidates,
1611
+ max_candidates_semantics: "passed_candidates",
1612
+ detail_limit: normalizedDetailLimit,
1613
+ close_detail: closeDetail,
1614
+ cv_acquisition_mode: cvAcquisitionMode,
1615
+ max_image_pages: maxImagePages,
1616
+ image_wheel_delta_y: imageWheelDeltaY,
1617
+ list_max_scrolls: listMaxScrolls,
1618
+ list_stable_signature_limit: listStableSignatureLimit,
1619
+ list_wheel_delta_y: listWheelDeltaY,
1620
+ list_settle_ms: listSettleMs,
1621
+ list_fallback_point: listFallbackPoint,
1622
+ refresh_on_end: refreshOnEnd,
1623
+ max_refresh_rounds: maxRefreshRounds,
1624
+ refresh_button_settle_ms: refreshButtonSettleMs,
1625
+ refresh_reload_settle_ms: refreshReloadSettleMs,
1626
+ post_action: normalizedPostAction,
1627
+ max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
1628
+ execute_post_action: Boolean(executePostAction),
1629
+ action_timeout_ms: actionTimeoutMs,
1630
+ screening_mode: normalizedScreeningMode,
1631
+ llm_configured: Boolean(llmConfig),
1567
1632
  llm_timeout_ms: llmTimeoutMs,
1568
1633
  llm_image_limit: llmImageLimit,
1569
1634
  llm_image_detail: llmImageDetail,
@@ -1574,16 +1639,16 @@ export function createRecommendRunService({
1574
1639
  human_rest_enabled: effectiveHumanRestEnabled
1575
1640
  },
1576
1641
  progress: {
1577
- card_count: 0,
1578
- target_count: candidateLimit,
1579
- target_count_semantics: "passed_candidates",
1580
- processed: 0,
1581
- screened: 0,
1582
- detail_opened: 0,
1583
- llm_screened: 0,
1584
- passed: 0,
1585
- greet_count: 0,
1586
- post_action_clicked: 0,
1642
+ card_count: 0,
1643
+ target_count: candidateLimit,
1644
+ target_count_semantics: "passed_candidates",
1645
+ processed: 0,
1646
+ screened: 0,
1647
+ detail_opened: 0,
1648
+ llm_screened: 0,
1649
+ passed: 0,
1650
+ greet_count: 0,
1651
+ post_action_clicked: 0,
1587
1652
  image_capture_failed: 0,
1588
1653
  detail_open_failed: 0,
1589
1654
  transient_recovered: 0,
@@ -1595,40 +1660,40 @@ export function createRecommendRunService({
1595
1660
  human_rest_ms: 0,
1596
1661
  last_human_event: null
1597
1662
  },
1598
- checkpoint: {},
1599
- task: (runControl) => workflow({
1600
- client,
1601
- targetUrl,
1602
- criteria,
1603
- jobLabel,
1604
- pageScope: requestedPageScope,
1605
- fallbackPageScope: normalizedFallbackPageScope,
1606
- filter: normalizedFilter,
1607
- maxCandidates,
1608
- detailLimit: normalizedDetailLimit,
1609
- closeDetail,
1610
- delayMs,
1611
- cardTimeoutMs,
1612
- maxImagePages,
1613
- imageWheelDeltaY,
1614
- cvAcquisitionMode,
1615
- listMaxScrolls,
1616
- listStableSignatureLimit,
1617
- listWheelDeltaY,
1618
- listSettleMs,
1619
- listFallbackPoint,
1620
- refreshOnEnd,
1621
- maxRefreshRounds,
1622
- refreshButtonSettleMs,
1623
- refreshReloadSettleMs,
1624
- postAction: normalizedPostAction,
1625
- maxGreetCount,
1626
- executePostAction,
1627
- actionTimeoutMs,
1628
- actionIntervalMs,
1629
- actionAfterClickDelayMs,
1630
- screeningMode: normalizedScreeningMode,
1631
- llmConfig,
1663
+ checkpoint: {},
1664
+ task: (runControl) => workflow({
1665
+ client,
1666
+ targetUrl,
1667
+ criteria,
1668
+ jobLabel,
1669
+ pageScope: requestedPageScope,
1670
+ fallbackPageScope: normalizedFallbackPageScope,
1671
+ filter: normalizedFilter,
1672
+ maxCandidates,
1673
+ detailLimit: normalizedDetailLimit,
1674
+ closeDetail,
1675
+ delayMs,
1676
+ cardTimeoutMs,
1677
+ maxImagePages,
1678
+ imageWheelDeltaY,
1679
+ cvAcquisitionMode,
1680
+ listMaxScrolls,
1681
+ listStableSignatureLimit,
1682
+ listWheelDeltaY,
1683
+ listSettleMs,
1684
+ listFallbackPoint,
1685
+ refreshOnEnd,
1686
+ maxRefreshRounds,
1687
+ refreshButtonSettleMs,
1688
+ refreshReloadSettleMs,
1689
+ postAction: normalizedPostAction,
1690
+ maxGreetCount,
1691
+ executePostAction,
1692
+ actionTimeoutMs,
1693
+ actionIntervalMs,
1694
+ actionAfterClickDelayMs,
1695
+ screeningMode: normalizedScreeningMode,
1696
+ llmConfig,
1632
1697
  llmTimeoutMs,
1633
1698
  llmImageLimit,
1634
1699
  llmImageDetail,
@@ -1638,15 +1703,15 @@ export function createRecommendRunService({
1638
1703
  }, runControl)
1639
1704
  });
1640
1705
  }
1641
-
1642
- return {
1643
- startRecommendRun,
1644
- getRecommendRun: manager.getRun,
1645
- pauseRecommendRun: manager.pauseRun,
1646
- resumeRecommendRun: manager.resumeRun,
1647
- cancelRecommendRun: manager.cancelRun,
1648
- waitForRecommendRun: manager.waitForRun,
1649
- listRecommendRuns: manager.listRuns,
1650
- manager
1651
- };
1652
- }
1706
+
1707
+ return {
1708
+ startRecommendRun,
1709
+ getRecommendRun: manager.getRun,
1710
+ pauseRecommendRun: manager.pauseRun,
1711
+ resumeRecommendRun: manager.resumeRun,
1712
+ cancelRecommendRun: manager.cancelRun,
1713
+ waitForRecommendRun: manager.waitForRun,
1714
+ listRecommendRuns: manager.listRuns,
1715
+ manager
1716
+ };
1717
+ }