@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -0,0 +1,903 @@
1
+ import { createRunLifecycleManager } from "../../core/run/index.js";
2
+ import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
3
+ import { sleep } from "../../core/browser/index.js";
4
+ import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
5
+ import {
6
+ compactCvAcquisitionState,
7
+ countParsedNetworkProfiles,
8
+ createCvAcquisitionState,
9
+ getCvNetworkWaitPlan,
10
+ recordCvImageFallback,
11
+ recordCvNetworkHit,
12
+ recordCvNetworkMiss,
13
+ summarizeImageEvidence,
14
+ waitForCvNetworkEvents
15
+ } from "../../core/cv-acquisition/index.js";
16
+ import {
17
+ compactInfiniteListState,
18
+ createInfiniteListState,
19
+ getNextInfiniteListCandidate,
20
+ markInfiniteListCandidateProcessed,
21
+ resetInfiniteListForRefreshRound
22
+ } from "../../core/infinite-list/index.js";
23
+ import { screenCandidate } from "../../core/screening/index.js";
24
+ import {
25
+ closeRecommendDetail,
26
+ createRecommendDetailNetworkRecorder,
27
+ extractRecommendDetailCandidate,
28
+ openRecommendCardDetail,
29
+ waitForRecommendDetailNetworkEvents
30
+ } from "./detail.js";
31
+ import {
32
+ readRecommendCardCandidate,
33
+ waitForRecommendCardNodeIds
34
+ } from "./cards.js";
35
+ import { selectAndConfirmFirstSafeFilter } from "./filters.js";
36
+ import {
37
+ buildRecommendFilterSelectionOptions,
38
+ refreshRecommendListAtEnd
39
+ } from "./refresh.js";
40
+ import { selectRecommendJob } from "./jobs.js";
41
+ import {
42
+ normalizeRecommendPageScope,
43
+ selectRecommendPageScope
44
+ } from "./scopes.js";
45
+ import {
46
+ clickRecommendActionControl,
47
+ normalizeRecommendPostAction,
48
+ resolveRecommendPostAction,
49
+ waitForRecommendDetailActionControls
50
+ } from "./actions.js";
51
+ import { getRecommendRoots } from "./roots.js";
52
+
53
+ function normalizeLabels(labels = []) {
54
+ return labels.map((label) => String(label || "").trim()).filter(Boolean);
55
+ }
56
+
57
+ function normalizeFilter(filter = {}) {
58
+ const filterGroups = Array.isArray(filter.filterGroups)
59
+ ? filter.filterGroups
60
+ : Array.isArray(filter.groups)
61
+ ? filter.groups
62
+ : [];
63
+ return {
64
+ enabled: filter.enabled !== false,
65
+ group: String(filter.group || ""),
66
+ labels: normalizeLabels(filter.labels || filter.filterLabels || []),
67
+ selectAllLabels: Boolean(filter.selectAllLabels),
68
+ filterGroups: filterGroups.map((group) => ({
69
+ group: String(group?.group || ""),
70
+ labels: normalizeLabels(group?.labels || group?.filterLabels || []),
71
+ selectAllLabels: group?.selectAllLabels !== false
72
+ })).filter((group) => group.group || group.labels.length)
73
+ };
74
+ }
75
+
76
+ function compactFilterResult(filterResult) {
77
+ if (!filterResult) return null;
78
+ return {
79
+ opened_panel: Boolean(filterResult.opened_panel),
80
+ selected_option: filterResult.selected_option
81
+ ? {
82
+ group: filterResult.selected_option.group,
83
+ label: filterResult.selected_option.label,
84
+ was_active: Boolean(filterResult.selected_option.was_active),
85
+ clicked: filterResult.selected_option.clicked !== false
86
+ }
87
+ : null,
88
+ selected_options: (filterResult.selected_options || []).map((option) => ({
89
+ group: option.group,
90
+ label: option.label,
91
+ was_active: Boolean(option.was_active),
92
+ clicked: option.clicked !== false
93
+ })),
94
+ confirmed: Boolean(filterResult.confirmed),
95
+ before_counts: filterResult.before_counts,
96
+ after_confirm_counts: filterResult.after_confirm_counts
97
+ };
98
+ }
99
+
100
+ function compactJobSelection(jobSelection) {
101
+ if (!jobSelection) return null;
102
+ return {
103
+ requested: jobSelection.requested || "",
104
+ selected: Boolean(jobSelection.selected),
105
+ already_current: Boolean(jobSelection.already_current),
106
+ reason: jobSelection.reason || null,
107
+ selected_option: jobSelection.selected_option || null,
108
+ options: (jobSelection.options || []).map((option) => ({
109
+ label: option.label,
110
+ label_without_salary: option.label_without_salary,
111
+ current: Boolean(option.current),
112
+ visible: Boolean(option.visible),
113
+ class_name: option.class_name
114
+ }))
115
+ };
116
+ }
117
+
118
+ function compactPageScopeSelection(pageScopeSelection) {
119
+ if (!pageScopeSelection) return null;
120
+ return {
121
+ requested_scope: pageScopeSelection.requested_scope || null,
122
+ effective_scope: pageScopeSelection.effective_scope || null,
123
+ fallback_scope: pageScopeSelection.fallback_scope || null,
124
+ fallback_applied: Boolean(pageScopeSelection.fallback_applied),
125
+ selected: Boolean(pageScopeSelection.selected),
126
+ already_current: Boolean(pageScopeSelection.already_current),
127
+ reason: pageScopeSelection.reason || null,
128
+ selected_tab: pageScopeSelection.selected_tab || null,
129
+ available_scopes: pageScopeSelection.available_scopes || [],
130
+ card_count: pageScopeSelection.after?.card_count || null
131
+ };
132
+ }
133
+
134
+ function compactScreening(screening) {
135
+ return {
136
+ status: screening.status,
137
+ passed: screening.passed,
138
+ score: screening.score,
139
+ reasons: screening.reasons,
140
+ candidate: {
141
+ domain: screening.candidate?.domain || "recommend",
142
+ source: screening.candidate?.source || "",
143
+ id: screening.candidate?.id || null,
144
+ identity: screening.candidate?.identity || {}
145
+ }
146
+ };
147
+ }
148
+
149
+ function compactCandidate(candidate) {
150
+ return {
151
+ id: candidate?.id || null,
152
+ identity: candidate?.identity || {},
153
+ text_length: candidate?.text?.raw?.length || 0,
154
+ tag_count: candidate?.tags?.length || 0
155
+ };
156
+ }
157
+
158
+ function compactDetail(detailResult) {
159
+ if (!detailResult) return null;
160
+ return {
161
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
162
+ resume_text_length: detailResult.detail?.resume_text?.length || 0,
163
+ network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
164
+ parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
165
+ cv_acquisition: detailResult.cv_acquisition || null,
166
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
167
+ close_result: detailResult.close_result
168
+ };
169
+ }
170
+
171
+ function compactActionDiscovery(discovery) {
172
+ if (!discovery) return null;
173
+ return {
174
+ elapsed_ms: discovery.elapsed_ms,
175
+ timed_out: Boolean(discovery.timed_out),
176
+ detail_root_count: discovery.detail_root_count || 0,
177
+ summary: discovery.summary || null
178
+ };
179
+ }
180
+
181
+ async function runRecommendPostAction({
182
+ client,
183
+ screening,
184
+ actionDiscovery,
185
+ postAction = "none",
186
+ greetCount = 0,
187
+ maxGreetCount = null,
188
+ executePostAction = true,
189
+ afterClickDelayMs = 900
190
+ } = {}) {
191
+ const plan = resolveRecommendPostAction({
192
+ postAction,
193
+ greetCount,
194
+ maxGreetCount
195
+ });
196
+ const result = {
197
+ requested: postAction,
198
+ execute_post_action: Boolean(executePostAction),
199
+ plan,
200
+ eligible: Boolean(screening?.passed),
201
+ action_attempted: false,
202
+ action_clicked: false,
203
+ counted_as_greet: false,
204
+ reason: ""
205
+ };
206
+
207
+ if (!screening?.passed) {
208
+ result.reason = "screening_not_passed";
209
+ return result;
210
+ }
211
+ if (plan.effective === "none") {
212
+ result.reason = "post_action_none";
213
+ return result;
214
+ }
215
+
216
+ const summary = actionDiscovery?.summary || {};
217
+ const control = plan.effective === "favorite" ? summary.favorite : summary.greet;
218
+ if (!control?.found) {
219
+ result.reason = `${plan.effective}_control_not_found`;
220
+ return result;
221
+ }
222
+ result.control = control;
223
+
224
+ if (plan.effective === "greet" && control.continue_chat) {
225
+ result.reason = "already_connected_continue_chat";
226
+ result.already_connected = true;
227
+ return result;
228
+ }
229
+ if (plan.effective === "greet" && control.greet_quota?.exhausted) {
230
+ result.reason = "greet_credits_exhausted";
231
+ result.out_of_greet_credits = true;
232
+ result.stop_run = true;
233
+ return result;
234
+ }
235
+ if (plan.effective === "greet" && control.available === false) {
236
+ result.reason = "greet_control_not_available";
237
+ return result;
238
+ }
239
+ if (plan.effective === "favorite" && control.active) {
240
+ result.reason = "already_favorited";
241
+ result.already_favorited = true;
242
+ return result;
243
+ }
244
+ if (control.disabled) {
245
+ result.reason = `${plan.effective}_control_disabled`;
246
+ return result;
247
+ }
248
+ if (!executePostAction) {
249
+ result.reason = "dry_run_post_action";
250
+ result.would_click = true;
251
+ return result;
252
+ }
253
+
254
+ result.action_attempted = true;
255
+ result.control_before = control;
256
+ let clickResult;
257
+ try {
258
+ clickResult = await clickRecommendActionControl(client, {
259
+ ...control,
260
+ kind: plan.effective
261
+ });
262
+ } catch (error) {
263
+ if (error?.code === GREET_CREDITS_EXHAUSTED_CODE) {
264
+ result.reason = "greet_credits_exhausted";
265
+ result.out_of_greet_credits = true;
266
+ result.stop_run = true;
267
+ result.greet_quota = error.greet_quota || control.greet_quota || null;
268
+ return result;
269
+ }
270
+ throw error;
271
+ }
272
+ result.click_result = clickResult;
273
+ result.action_clicked = true;
274
+ result.counted_as_greet = plan.effective === "greet";
275
+ result.reason = "clicked";
276
+ if (afterClickDelayMs > 0) await sleep(afterClickDelayMs);
277
+ try {
278
+ const afterDiscovery = await waitForRecommendDetailActionControls(client, {
279
+ timeoutMs: 2500,
280
+ intervalMs: 300,
281
+ requireAny: false
282
+ });
283
+ const afterSummary = afterDiscovery?.summary || {};
284
+ const afterControl = plan.effective === "favorite" ? afterSummary.favorite : afterSummary.greet;
285
+ result.action_discovery_after = compactActionDiscovery(afterDiscovery);
286
+ result.control_after = afterControl || null;
287
+ if (plan.effective === "greet") {
288
+ result.verified_after_click = Boolean(
289
+ afterControl?.continue_chat
290
+ || String(afterControl?.label || "").includes("继续沟通")
291
+ );
292
+ } else if (plan.effective === "favorite") {
293
+ result.verified_after_click = Boolean(
294
+ afterControl?.active
295
+ || String(afterControl?.label || "").includes("已")
296
+ );
297
+ }
298
+ } catch (error) {
299
+ result.verify_error = {
300
+ message: error?.message || String(error)
301
+ };
302
+ }
303
+ return result;
304
+ }
305
+
306
+ function compactRefreshAttempt(refreshAttempt) {
307
+ if (!refreshAttempt) return null;
308
+ return {
309
+ ok: Boolean(refreshAttempt.ok),
310
+ method: refreshAttempt.method || "",
311
+ forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
312
+ card_count: refreshAttempt.card_count || 0,
313
+ attempts: (refreshAttempt.attempts || []).map((attempt) => ({
314
+ ok: Boolean(attempt.ok),
315
+ method: attempt.method || "",
316
+ reason: attempt.reason || null,
317
+ label: attempt.label || null,
318
+ before_card_count: attempt.before_card_count || 0,
319
+ after_card_count: attempt.after_card_count || 0
320
+ })),
321
+ page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
322
+ filter: compactFilterResult(refreshAttempt.filter)
323
+ };
324
+ }
325
+
326
+ export async function runRecommendWorkflow({
327
+ client,
328
+ targetUrl = "",
329
+ criteria = "",
330
+ jobLabel = "",
331
+ pageScope = "recommend",
332
+ fallbackPageScope = "recommend",
333
+ filter = {},
334
+ maxCandidates = 5,
335
+ detailLimit = 0,
336
+ closeDetail = true,
337
+ delayMs = 0,
338
+ cardTimeoutMs = 10000,
339
+ maxImagePages = 8,
340
+ imageWheelDeltaY = 650,
341
+ cvAcquisitionMode = "unknown",
342
+ listMaxScrolls = 20,
343
+ listStableSignatureLimit = 2,
344
+ listWheelDeltaY = 850,
345
+ listSettleMs = 1200,
346
+ listFallbackPoint = null,
347
+ refreshOnEnd = true,
348
+ maxRefreshRounds = 2,
349
+ refreshButtonSettleMs = 8000,
350
+ refreshReloadSettleMs = 8000,
351
+ postAction = "none",
352
+ maxGreetCount = null,
353
+ executePostAction = true,
354
+ actionTimeoutMs = 8000,
355
+ actionIntervalMs = 500,
356
+ actionAfterClickDelayMs = 900
357
+ } = {}, runControl) {
358
+ if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
359
+ const normalizedFilter = normalizeFilter(filter);
360
+ const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
361
+ const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
362
+ const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
363
+ const postActionEnabled = normalizedPostAction !== "none";
364
+ const limit = Math.max(1, Number(maxCandidates) || 1);
365
+ const detailCountLimit = Math.max(0, Number(detailLimit) || 0);
366
+ const effectiveDetailLimit = postActionEnabled ? limit : detailCountLimit;
367
+ const networkRecorder = effectiveDetailLimit > 0
368
+ ? createRecommendDetailNetworkRecorder(client)
369
+ : null;
370
+ const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
371
+ const listState = createInfiniteListState({
372
+ domain: "recommend",
373
+ listName: "recommend-candidates"
374
+ });
375
+ const results = [];
376
+ const refreshAttempts = [];
377
+ let refreshRounds = 0;
378
+ let greetCount = 0;
379
+ let jobSelection = null;
380
+ let pageScopeSelection = null;
381
+ let filterResult = null;
382
+ let cardNodeIds = [];
383
+ let listEndReason = "";
384
+
385
+ runControl.setPhase("recommend:cleanup");
386
+ await closeRecommendDetail(client, { attemptsLimit: 2 });
387
+
388
+ await runControl.waitIfPaused();
389
+ runControl.throwIfCanceled();
390
+ runControl.setPhase("recommend:roots");
391
+ let rootState = await getRecommendRoots(client);
392
+ runControl.checkpoint({
393
+ iframe_selector: rootState.iframe.selector,
394
+ iframe_document_node_id: rootState.iframe.documentNodeId
395
+ });
396
+
397
+ if (jobLabel) {
398
+ await runControl.waitIfPaused();
399
+ runControl.throwIfCanceled();
400
+ runControl.setPhase("recommend:job");
401
+ jobSelection = await selectRecommendJob(client, rootState.iframe.documentNodeId, {
402
+ jobLabel,
403
+ settleMs: cardTimeoutMs > 45000 ? 12000 : 6000
404
+ });
405
+ if (!jobSelection.selected) {
406
+ throw new Error(`Requested recommend job was not selected: ${jobSelection.reason}`);
407
+ }
408
+ rootState = await getRecommendRoots(client);
409
+ runControl.checkpoint({
410
+ job_selection: compactJobSelection(jobSelection)
411
+ });
412
+ }
413
+
414
+ await runControl.waitIfPaused();
415
+ runControl.throwIfCanceled();
416
+ runControl.setPhase("recommend:page-scope");
417
+ pageScopeSelection = await selectRecommendPageScope(client, rootState.iframe.documentNodeId, {
418
+ pageScope: requestedPageScope,
419
+ fallbackScope: normalizedFallbackPageScope,
420
+ settleMs: cardTimeoutMs > 45000 ? 3000 : 1200,
421
+ timeoutMs: Math.min(Math.max(cardTimeoutMs, 10000), 60000)
422
+ });
423
+ if (!pageScopeSelection.selected) {
424
+ throw new Error(`Recommend page scope was not selected: ${pageScopeSelection.reason || pageScopeSelection.effective_scope || requestedPageScope}`);
425
+ }
426
+ rootState = await getRecommendRoots(client);
427
+ runControl.checkpoint({
428
+ page_scope: compactPageScopeSelection(pageScopeSelection)
429
+ });
430
+
431
+ if (normalizedFilter.enabled) {
432
+ await runControl.waitIfPaused();
433
+ runControl.throwIfCanceled();
434
+ runControl.setPhase("recommend:filter");
435
+ filterResult = await selectAndConfirmFirstSafeFilter(
436
+ client,
437
+ rootState.iframe.documentNodeId,
438
+ buildRecommendFilterSelectionOptions(normalizedFilter)
439
+ );
440
+ if (!filterResult.confirmed) {
441
+ throw new Error("Recommend run filter selection was not confirmed");
442
+ }
443
+ runControl.checkpoint({
444
+ filter: compactFilterResult(filterResult)
445
+ });
446
+ }
447
+
448
+ await runControl.waitIfPaused();
449
+ runControl.throwIfCanceled();
450
+ runControl.setPhase("recommend:cards");
451
+ cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
452
+ timeoutMs: cardTimeoutMs,
453
+ intervalMs: 300
454
+ });
455
+ if (!cardNodeIds.length) {
456
+ throw new Error("No recommend candidate cards found for run service");
457
+ }
458
+
459
+ runControl.updateProgress({
460
+ card_count: cardNodeIds.length,
461
+ target_count: limit,
462
+ processed: 0,
463
+ screened: 0,
464
+ detail_opened: 0,
465
+ passed: 0,
466
+ greet_count: 0,
467
+ post_action_clicked: 0,
468
+ unique_seen: compactInfiniteListState(listState).seen_count,
469
+ scroll_count: 0,
470
+ refresh_rounds: 0,
471
+ refresh_attempts: 0
472
+ });
473
+
474
+ while (results.length < limit) {
475
+ await runControl.waitIfPaused();
476
+ runControl.throwIfCanceled();
477
+ runControl.setPhase("recommend:candidate");
478
+
479
+ const nextCandidateResult = await getNextInfiniteListCandidate({
480
+ client,
481
+ state: listState,
482
+ maxScrolls: listMaxScrolls,
483
+ stableSignatureLimit: listStableSignatureLimit,
484
+ wheelDeltaY: listWheelDeltaY,
485
+ settleMs: listSettleMs,
486
+ fallbackPoint: listFallbackPoint,
487
+ findNodeIds: async () => {
488
+ const currentRootState = await getRecommendRoots(client);
489
+ const currentCardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
490
+ timeoutMs: Math.min(cardTimeoutMs, 5000),
491
+ intervalMs: 300
492
+ });
493
+ cardNodeIds = currentCardNodeIds;
494
+ return currentCardNodeIds;
495
+ },
496
+ readCandidate: async (nodeId, { visibleIndex }) => readRecommendCardCandidate(client, nodeId, {
497
+ targetUrl,
498
+ source: "recommend-run-card",
499
+ metadata: {
500
+ run_candidate_index: results.length,
501
+ visible_index: visibleIndex
502
+ }
503
+ })
504
+ });
505
+ if (!nextCandidateResult.ok) {
506
+ listEndReason = nextCandidateResult.reason || "list_exhausted";
507
+ if (
508
+ nextCandidateResult.end_reached
509
+ && refreshOnEnd
510
+ && results.length < limit
511
+ && refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
512
+ ) {
513
+ await runControl.waitIfPaused();
514
+ runControl.throwIfCanceled();
515
+ runControl.setPhase("recommend:refresh");
516
+ refreshRounds += 1;
517
+ const refreshResult = await refreshRecommendListAtEnd(client, {
518
+ rootState,
519
+ jobLabel,
520
+ pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
521
+ fallbackPageScope: normalizedFallbackPageScope,
522
+ filter: normalizedFilter,
523
+ forceRecentNotView: true,
524
+ cardTimeoutMs,
525
+ buttonSettleMs: refreshButtonSettleMs,
526
+ reloadSettleMs: refreshReloadSettleMs
527
+ });
528
+ const compactRefresh = compactRefreshAttempt(refreshResult);
529
+ refreshAttempts.push(compactRefresh);
530
+ runControl.checkpoint({
531
+ refresh_round: refreshRounds,
532
+ refresh: compactRefresh
533
+ });
534
+ runControl.updateProgress({
535
+ card_count: refreshResult.card_count || cardNodeIds.length,
536
+ target_count: limit,
537
+ processed: results.length,
538
+ screened: results.length,
539
+ detail_opened: results.filter((item) => item.detail).length,
540
+ passed: results.filter((item) => item.screening.passed).length,
541
+ unique_seen: compactInfiniteListState(listState).seen_count,
542
+ scroll_count: compactInfiniteListState(listState).scroll_count,
543
+ refresh_rounds: refreshRounds,
544
+ refresh_attempts: refreshAttempts.length,
545
+ refresh_method: refreshResult.method || null,
546
+ refresh_forced_recent_not_view: true,
547
+ list_end_reason: listEndReason
548
+ });
549
+ if (refreshResult.ok) {
550
+ rootState = refreshResult.root_state || await getRecommendRoots(client);
551
+ cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
552
+ timeoutMs: cardTimeoutMs,
553
+ intervalMs: 300
554
+ });
555
+ resetInfiniteListForRefreshRound(listState, {
556
+ reason: listEndReason,
557
+ round: refreshRounds,
558
+ method: refreshResult.method,
559
+ metadata: {
560
+ card_count: cardNodeIds.length,
561
+ forced_recent_not_view: true
562
+ }
563
+ });
564
+ listEndReason = "";
565
+ continue;
566
+ }
567
+ }
568
+ break;
569
+ }
570
+
571
+ const index = results.length;
572
+ const cardNodeId = nextCandidateResult.item.node_id;
573
+ const candidateKey = nextCandidateResult.item.key;
574
+ const cardCandidate = nextCandidateResult.item.candidate;
575
+
576
+ let screeningCandidate = cardCandidate;
577
+ let detailResult = null;
578
+ if (index < effectiveDetailLimit) {
579
+ await runControl.waitIfPaused();
580
+ runControl.throwIfCanceled();
581
+ runControl.setPhase("recommend:detail");
582
+ networkRecorder.clear();
583
+ const openedDetail = await openRecommendCardDetail(client, cardNodeId);
584
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
585
+ const networkWait = await waitForCvNetworkEvents(
586
+ waitForRecommendDetailNetworkEvents,
587
+ networkRecorder,
588
+ {
589
+ waitPlan,
590
+ minCount: 1,
591
+ requireLoaded: true,
592
+ intervalMs: 120
593
+ }
594
+ );
595
+ detailResult = await extractRecommendDetailCandidate(client, {
596
+ cardCandidate,
597
+ cardNodeId,
598
+ detailState: openedDetail.detail_state,
599
+ networkEvents: networkRecorder.events,
600
+ targetUrl,
601
+ closeDetail: false
602
+ });
603
+
604
+ const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
605
+ let source = "network";
606
+ let imageEvidence = null;
607
+ if (parsedNetworkProfileCount > 0) {
608
+ recordCvNetworkHit(cvAcquisitionState, {
609
+ parsedNetworkProfileCount,
610
+ waitResult: networkWait
611
+ });
612
+ } else {
613
+ const captureNodeId = openedDetail.detail_state?.popup?.node_id
614
+ || openedDetail.detail_state?.resumeIframe?.node_id
615
+ || null;
616
+ if (captureNodeId) {
617
+ imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
618
+ padding: 4,
619
+ maxScreenshots: maxImagePages,
620
+ wheelDeltaY: imageWheelDeltaY,
621
+ settleMs: 1200,
622
+ metadata: {
623
+ domain: "recommend",
624
+ capture_mode: "scroll_sequence",
625
+ acquisition_reason: "network_miss_image_fallback",
626
+ run_candidate_index: index,
627
+ candidate_key: candidateKey
628
+ }
629
+ });
630
+ source = "image";
631
+ recordCvImageFallback(cvAcquisitionState, {
632
+ parsedNetworkProfileCount,
633
+ waitResult: networkWait,
634
+ imageEvidence
635
+ });
636
+ } else {
637
+ source = "missing_capture_node";
638
+ recordCvNetworkMiss(cvAcquisitionState, {
639
+ reason: "network_miss_no_capture_node",
640
+ parsedNetworkProfileCount,
641
+ waitResult: networkWait
642
+ });
643
+ }
644
+ }
645
+
646
+ detailResult.image_evidence = imageEvidence;
647
+ detailResult.cv_acquisition = {
648
+ source,
649
+ mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
650
+ wait_plan: waitPlan,
651
+ network_wait: networkWait,
652
+ parsed_network_profile_count: parsedNetworkProfileCount,
653
+ image_evidence: summarizeImageEvidence(imageEvidence)
654
+ };
655
+ screeningCandidate = detailResult.candidate;
656
+ }
657
+
658
+ await runControl.waitIfPaused();
659
+ runControl.throwIfCanceled();
660
+ runControl.setPhase("recommend:screening");
661
+ const screening = screenCandidate(screeningCandidate, { criteria });
662
+ let actionDiscovery = null;
663
+ let postActionResult = null;
664
+ if (postActionEnabled && detailResult) {
665
+ await runControl.waitIfPaused();
666
+ runControl.throwIfCanceled();
667
+ runControl.setPhase("recommend:post-action");
668
+ actionDiscovery = await waitForRecommendDetailActionControls(client, {
669
+ timeoutMs: actionTimeoutMs,
670
+ intervalMs: actionIntervalMs,
671
+ requireAny: true
672
+ });
673
+ postActionResult = await runRecommendPostAction({
674
+ client,
675
+ screening,
676
+ actionDiscovery,
677
+ postAction: normalizedPostAction,
678
+ greetCount,
679
+ maxGreetCount: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
680
+ executePostAction,
681
+ afterClickDelayMs: actionAfterClickDelayMs
682
+ });
683
+ if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
684
+ greetCount += 1;
685
+ }
686
+ }
687
+ if (detailResult && closeDetail) {
688
+ detailResult.close_result = await closeRecommendDetail(client);
689
+ }
690
+ const compactResult = {
691
+ index,
692
+ candidate_key: candidateKey,
693
+ card_node_id: cardNodeId,
694
+ candidate: compactCandidate(screeningCandidate),
695
+ detail: compactDetail(detailResult),
696
+ screening: compactScreening(screening),
697
+ action_discovery: compactActionDiscovery(actionDiscovery),
698
+ post_action: postActionResult
699
+ };
700
+ results.push(compactResult);
701
+ markInfiniteListCandidateProcessed(listState, candidateKey, {
702
+ metadata: {
703
+ result_index: index,
704
+ candidate_id: screeningCandidate.id || null
705
+ }
706
+ });
707
+
708
+ runControl.updateProgress({
709
+ card_count: cardNodeIds.length,
710
+ target_count: limit,
711
+ processed: results.length,
712
+ screened: results.length,
713
+ detail_opened: results.filter((item) => item.detail).length,
714
+ passed: results.filter((item) => item.screening.passed).length,
715
+ greet_count: greetCount,
716
+ post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
717
+ unique_seen: compactInfiniteListState(listState).seen_count,
718
+ scroll_count: compactInfiniteListState(listState).scroll_count,
719
+ refresh_rounds: refreshRounds,
720
+ refresh_attempts: refreshAttempts.length,
721
+ list_end_reason: listEndReason || null,
722
+ last_candidate_id: screeningCandidate.id || null,
723
+ last_candidate_key: candidateKey,
724
+ last_score: screening.score
725
+ });
726
+ runControl.checkpoint({
727
+ last_candidate: {
728
+ id: screeningCandidate.id || null,
729
+ key: candidateKey,
730
+ identity: screeningCandidate.identity || {},
731
+ screening: {
732
+ status: screening.status,
733
+ passed: screening.passed,
734
+ score: screening.score
735
+ },
736
+ post_action: postActionResult
737
+ }
738
+ });
739
+
740
+ if (postActionResult?.stop_run) {
741
+ listEndReason = postActionResult.reason || "post_action_stop";
742
+ break;
743
+ }
744
+
745
+ if (delayMs > 0) {
746
+ await runControl.sleep(delayMs);
747
+ }
748
+ }
749
+
750
+ runControl.setPhase("recommend:done");
751
+ return {
752
+ domain: "recommend",
753
+ target_url: targetUrl,
754
+ job_selection: compactJobSelection(jobSelection),
755
+ page_scope: compactPageScopeSelection(pageScopeSelection),
756
+ filter: compactFilterResult(filterResult),
757
+ card_count: cardNodeIds.length,
758
+ candidate_list: compactInfiniteListState(listState),
759
+ list_end_reason: listEndReason || null,
760
+ refresh_rounds: refreshRounds,
761
+ refresh_attempts: refreshAttempts,
762
+ processed: results.length,
763
+ screened: results.length,
764
+ detail_opened: results.filter((item) => item.detail).length,
765
+ passed: results.filter((item) => item.screening.passed).length,
766
+ greet_count: greetCount,
767
+ post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
768
+ results
769
+ };
770
+ }
771
+
772
+ export function createRecommendRunService({
773
+ lifecycle,
774
+ idPrefix = "recommend",
775
+ workflow = runRecommendWorkflow
776
+ } = {}) {
777
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix });
778
+
779
+ function startRecommendRun({
780
+ client,
781
+ targetUrl = "",
782
+ criteria = "",
783
+ jobLabel = "",
784
+ pageScope = "recommend",
785
+ fallbackPageScope = "recommend",
786
+ filter = {},
787
+ maxCandidates = 5,
788
+ detailLimit = 0,
789
+ closeDetail = true,
790
+ delayMs = 0,
791
+ cardTimeoutMs = 10000,
792
+ maxImagePages = 8,
793
+ imageWheelDeltaY = 650,
794
+ cvAcquisitionMode = "unknown",
795
+ listMaxScrolls = 20,
796
+ listStableSignatureLimit = 2,
797
+ listWheelDeltaY = 850,
798
+ listSettleMs = 1200,
799
+ listFallbackPoint = null,
800
+ refreshOnEnd = true,
801
+ maxRefreshRounds = 2,
802
+ refreshButtonSettleMs = 8000,
803
+ refreshReloadSettleMs = 8000,
804
+ postAction = "none",
805
+ maxGreetCount = null,
806
+ executePostAction = true,
807
+ actionTimeoutMs = 8000,
808
+ actionIntervalMs = 500,
809
+ actionAfterClickDelayMs = 900,
810
+ name = "recommend-domain-run"
811
+ } = {}) {
812
+ if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
813
+ const normalizedFilter = normalizeFilter(filter);
814
+ const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
815
+ const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
816
+ const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
817
+ return manager.startRun({
818
+ name,
819
+ context: {
820
+ domain: "recommend",
821
+ target_url: targetUrl,
822
+ criteria_present: Boolean(criteria),
823
+ job_label: jobLabel || "",
824
+ requested_page_scope: requestedPageScope,
825
+ fallback_page_scope: normalizedFallbackPageScope,
826
+ filter: normalizedFilter,
827
+ max_candidates: maxCandidates,
828
+ detail_limit: detailLimit,
829
+ close_detail: closeDetail,
830
+ cv_acquisition_mode: cvAcquisitionMode,
831
+ max_image_pages: maxImagePages,
832
+ image_wheel_delta_y: imageWheelDeltaY,
833
+ list_max_scrolls: listMaxScrolls,
834
+ list_stable_signature_limit: listStableSignatureLimit,
835
+ list_wheel_delta_y: listWheelDeltaY,
836
+ list_settle_ms: listSettleMs,
837
+ list_fallback_point: listFallbackPoint,
838
+ refresh_on_end: refreshOnEnd,
839
+ max_refresh_rounds: maxRefreshRounds,
840
+ refresh_button_settle_ms: refreshButtonSettleMs,
841
+ refresh_reload_settle_ms: refreshReloadSettleMs,
842
+ post_action: normalizedPostAction,
843
+ max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
844
+ execute_post_action: Boolean(executePostAction),
845
+ action_timeout_ms: actionTimeoutMs
846
+ },
847
+ progress: {
848
+ card_count: 0,
849
+ target_count: Math.max(1, Number(maxCandidates) || 1),
850
+ processed: 0,
851
+ screened: 0,
852
+ detail_opened: 0,
853
+ passed: 0,
854
+ greet_count: 0,
855
+ post_action_clicked: 0
856
+ },
857
+ checkpoint: {},
858
+ task: (runControl) => workflow({
859
+ client,
860
+ targetUrl,
861
+ criteria,
862
+ jobLabel,
863
+ pageScope: requestedPageScope,
864
+ fallbackPageScope: normalizedFallbackPageScope,
865
+ filter: normalizedFilter,
866
+ maxCandidates,
867
+ detailLimit,
868
+ closeDetail,
869
+ delayMs,
870
+ cardTimeoutMs,
871
+ maxImagePages,
872
+ imageWheelDeltaY,
873
+ cvAcquisitionMode,
874
+ listMaxScrolls,
875
+ listStableSignatureLimit,
876
+ listWheelDeltaY,
877
+ listSettleMs,
878
+ listFallbackPoint,
879
+ refreshOnEnd,
880
+ maxRefreshRounds,
881
+ refreshButtonSettleMs,
882
+ refreshReloadSettleMs,
883
+ postAction: normalizedPostAction,
884
+ maxGreetCount,
885
+ executePostAction,
886
+ actionTimeoutMs,
887
+ actionIntervalMs,
888
+ actionAfterClickDelayMs
889
+ }, runControl)
890
+ });
891
+ }
892
+
893
+ return {
894
+ startRecommendRun,
895
+ getRecommendRun: manager.getRun,
896
+ pauseRecommendRun: manager.pauseRun,
897
+ resumeRecommendRun: manager.resumeRun,
898
+ cancelRecommendRun: manager.cancelRun,
899
+ waitForRecommendRun: manager.waitForRun,
900
+ listRecommendRuns: manager.listRuns,
901
+ manager
902
+ };
903
+ }