@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +86 -33
  2. package/package.json +62 -9
  3. package/skills/boss-chat/SKILL.md +5 -4
  4. package/skills/boss-recommend-pipeline/SKILL.md +21 -31
  5. package/skills/boss-recruit-pipeline/README.md +17 -0
  6. package/skills/boss-recruit-pipeline/SKILL.md +55 -0
  7. package/src/chat-mcp.js +1333 -0
  8. package/src/chat-runtime-config.js +559 -0
  9. package/src/cli.js +1254 -225
  10. package/src/core/browser/index.js +378 -0
  11. package/src/core/capture/index.js +298 -0
  12. package/src/core/cv-acquisition/index.js +219 -0
  13. package/src/core/greet-quota/index.js +54 -0
  14. package/src/core/infinite-list/index.js +459 -0
  15. package/src/core/reporting/legacy-csv.js +332 -0
  16. package/src/core/run/index.js +286 -0
  17. package/src/core/screening/index.js +1166 -0
  18. package/src/core/self-heal/index.js +848 -0
  19. package/src/domains/chat/cards.js +129 -0
  20. package/src/domains/chat/constants.js +183 -0
  21. package/src/domains/chat/detail.js +1369 -0
  22. package/src/domains/chat/index.js +7 -0
  23. package/src/domains/chat/jobs.js +334 -0
  24. package/src/domains/chat/page-guard.js +88 -0
  25. package/src/domains/chat/roots.js +56 -0
  26. package/src/domains/chat/run-service.js +1101 -0
  27. package/src/domains/recommend/actions.js +457 -0
  28. package/src/domains/recommend/cards.js +228 -0
  29. package/src/domains/recommend/constants.js +141 -0
  30. package/src/domains/recommend/detail.js +341 -0
  31. package/src/domains/recommend/filters.js +581 -0
  32. package/src/domains/recommend/index.js +10 -0
  33. package/src/domains/recommend/jobs.js +232 -0
  34. package/src/domains/recommend/refresh.js +204 -0
  35. package/src/domains/recommend/roots.js +78 -0
  36. package/src/domains/recommend/run-service.js +903 -0
  37. package/src/domains/recommend/scopes.js +245 -0
  38. package/src/domains/recruit/actions.js +277 -0
  39. package/src/domains/recruit/cards.js +66 -0
  40. package/src/domains/recruit/constants.js +130 -0
  41. package/src/domains/recruit/detail.js +414 -0
  42. package/src/domains/recruit/index.js +9 -0
  43. package/src/domains/recruit/instruction-parser.js +451 -0
  44. package/src/domains/recruit/refresh.js +40 -0
  45. package/src/domains/recruit/roots.js +67 -0
  46. package/src/domains/recruit/run-service.js +580 -0
  47. package/src/domains/recruit/search.js +1149 -0
  48. package/src/index.js +578 -419
  49. package/src/recommend-mcp.js +1257 -0
  50. package/src/recruit-mcp.js +1035 -0
  51. package/src/adapters.js +0 -3079
  52. package/src/boss-chat.js +0 -1037
  53. package/src/pipeline.js +0 -2249
  54. package/src/recommend-healing-config.js +0 -131
  55. package/src/recommend-healing-rules.json +0 -261
  56. package/src/self-heal.js +0 -2237
  57. package/src/test-adapters-runtime.js +0 -628
  58. package/src/test-boss-chat.js +0 -3196
  59. package/src/test-index-async.js +0 -498
  60. package/src/test-parser.js +0 -742
  61. package/src/test-pipeline.js +0 -2703
  62. package/src/test-run-state.js +0 -152
  63. package/src/test-self-heal.js +0 -224
  64. package/vendor/boss-chat-cli/README.md +0 -134
  65. package/vendor/boss-chat-cli/package.json +0 -53
  66. package/vendor/boss-chat-cli/src/app.js +0 -1501
  67. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  68. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  69. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  70. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  71. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  72. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  73. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  74. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  75. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  76. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  77. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  78. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  79. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  80. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  81. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  82. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  83. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  84. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  85. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  86. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  87. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  88. 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
+ }