@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,580 @@
1
+ import { createRunLifecycleManager } from "../../core/run/index.js";
2
+ import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
3
+ import {
4
+ compactCvAcquisitionState,
5
+ countParsedNetworkProfiles,
6
+ createCvAcquisitionState,
7
+ getCvNetworkWaitPlan,
8
+ recordCvImageFallback,
9
+ recordCvNetworkHit,
10
+ recordCvNetworkMiss,
11
+ summarizeImageEvidence,
12
+ waitForCvNetworkEvents
13
+ } from "../../core/cv-acquisition/index.js";
14
+ import {
15
+ compactInfiniteListState,
16
+ createInfiniteListState,
17
+ getNextInfiniteListCandidate,
18
+ markInfiniteListCandidateProcessed,
19
+ resetInfiniteListForRefreshRound
20
+ } from "../../core/infinite-list/index.js";
21
+ import { screenCandidate } from "../../core/screening/index.js";
22
+ import {
23
+ closeRecruitDetail,
24
+ createRecruitDetailNetworkRecorder,
25
+ extractRecruitDetailCandidate,
26
+ openRecruitCardDetail,
27
+ waitForRecruitDetailNetworkEvents
28
+ } from "./detail.js";
29
+ import {
30
+ readRecruitCardCandidate,
31
+ waitForRecruitCardNodeIds
32
+ } from "./cards.js";
33
+ import {
34
+ applyRecruitSearchParams,
35
+ hasRecruitSearchParams,
36
+ normalizeRecruitSearchParams
37
+ } from "./search.js";
38
+ import { refreshRecruitSearchAtEnd } from "./refresh.js";
39
+ import { getRecruitRoots } from "./roots.js";
40
+
41
+ function compactScreening(screening) {
42
+ return {
43
+ status: screening.status,
44
+ passed: screening.passed,
45
+ score: screening.score,
46
+ reasons: screening.reasons,
47
+ candidate: {
48
+ domain: screening.candidate?.domain || "recruit",
49
+ source: screening.candidate?.source || "",
50
+ id: screening.candidate?.id || null,
51
+ identity: screening.candidate?.identity || {}
52
+ }
53
+ };
54
+ }
55
+
56
+ function compactCandidate(candidate) {
57
+ return {
58
+ id: candidate?.id || null,
59
+ identity: candidate?.identity || {},
60
+ text_length: candidate?.text?.raw?.length || 0,
61
+ tag_count: candidate?.tags?.length || 0
62
+ };
63
+ }
64
+
65
+ function compactDetail(detailResult) {
66
+ if (!detailResult) return null;
67
+ return {
68
+ popup_text_length: detailResult.detail?.popup_text?.length || 0,
69
+ resume_text_length: detailResult.detail?.resume_text?.length || 0,
70
+ network_body_count: detailResult.network_bodies?.filter((item) => item.body).length || 0,
71
+ parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
72
+ cv_acquisition: detailResult.cv_acquisition || null,
73
+ image_evidence: summarizeImageEvidence(detailResult.image_evidence),
74
+ close_result: detailResult.close_result
75
+ };
76
+ }
77
+
78
+ function normalizeSearchParams(searchParams = {}) {
79
+ return normalizeRecruitSearchParams(searchParams);
80
+ }
81
+
82
+ function compactRefreshAttempt(refreshAttempt) {
83
+ if (!refreshAttempt) return null;
84
+ return {
85
+ ok: Boolean(refreshAttempt.ok),
86
+ method: refreshAttempt.method || "",
87
+ forced_recent_viewed: Boolean(refreshAttempt.forced_recent_viewed),
88
+ card_count: refreshAttempt.card_count || 0,
89
+ search_params: refreshAttempt.search_params || null,
90
+ application: refreshAttempt.application
91
+ ? {
92
+ applied: Boolean(refreshAttempt.application.applied),
93
+ post_search_state: refreshAttempt.application.post_search_state,
94
+ steps: (refreshAttempt.application.steps || []).map((step) => ({
95
+ step: step.step,
96
+ applied: step.result?.applied,
97
+ clicked: step.result?.clicked,
98
+ searched: step.result?.searched,
99
+ reason: step.result?.reason || null
100
+ }))
101
+ }
102
+ : null
103
+ };
104
+ }
105
+
106
+ export async function runRecruitWorkflow({
107
+ client,
108
+ targetUrl = "",
109
+ criteria = "",
110
+ searchParams = {},
111
+ maxCandidates = 5,
112
+ detailLimit = 1,
113
+ closeDetail = true,
114
+ delayMs = 0,
115
+ cardTimeoutMs = 90000,
116
+ resetBeforeSearch = true,
117
+ resetTimeoutMs = 180000,
118
+ cityOptionTimeoutMs = 30000,
119
+ maxImagePages = 8,
120
+ imageWheelDeltaY = 650,
121
+ cvAcquisitionMode = "unknown",
122
+ listMaxScrolls = 20,
123
+ listStableSignatureLimit = 2,
124
+ listWheelDeltaY = 850,
125
+ listSettleMs = 1200,
126
+ listFallbackPoint = null,
127
+ refreshOnEnd = true,
128
+ maxRefreshRounds = 2,
129
+ refreshResetSettleMs = 5000
130
+ } = {}, runControl) {
131
+ if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
132
+ const normalizedSearchParams = normalizeSearchParams(searchParams);
133
+ const limit = Math.max(1, Number(maxCandidates) || 1);
134
+ const detailCountLimit = Math.max(0, Number(detailLimit) || 0);
135
+ const networkRecorder = detailCountLimit > 0
136
+ ? createRecruitDetailNetworkRecorder(client)
137
+ : null;
138
+ const cvAcquisitionState = createCvAcquisitionState({ mode: cvAcquisitionMode });
139
+ const listState = createInfiniteListState({
140
+ domain: "recruit",
141
+ listName: "search-results"
142
+ });
143
+ const results = [];
144
+ const refreshAttempts = [];
145
+ let refreshRounds = 0;
146
+ let cardNodeIds = [];
147
+ let listEndReason = "";
148
+
149
+ runControl.setPhase("recruit:cleanup");
150
+ await closeRecruitDetail(client, { attemptsLimit: 2 });
151
+
152
+ await runControl.waitIfPaused();
153
+ runControl.throwIfCanceled();
154
+ runControl.setPhase("recruit:roots");
155
+ let rootState = await getRecruitRoots(client);
156
+ runControl.checkpoint({
157
+ iframe_selector: rootState.iframe.selector,
158
+ iframe_document_node_id: rootState.iframe.documentNodeId,
159
+ search_params: normalizedSearchParams
160
+ });
161
+
162
+ if (hasRecruitSearchParams(normalizedSearchParams)) {
163
+ await runControl.waitIfPaused();
164
+ runControl.throwIfCanceled();
165
+ runControl.setPhase("recruit:search");
166
+ const searchResult = await applyRecruitSearchParams(client, {
167
+ searchParams: normalizedSearchParams,
168
+ requireCards: true,
169
+ resetBeforeApply: resetBeforeSearch,
170
+ searchTimeoutMs: cardTimeoutMs,
171
+ resetTimeoutMs,
172
+ cityOptionTimeoutMs
173
+ });
174
+ runControl.checkpoint({
175
+ search: {
176
+ search_params: searchResult.search_params,
177
+ before_counts: searchResult.before_counts,
178
+ post_search_state: searchResult.post_search_state,
179
+ steps: searchResult.steps.map((step) => ({
180
+ step: step.step,
181
+ applied: step.result?.applied,
182
+ clicked: step.result?.clicked,
183
+ searched: step.result?.searched,
184
+ reason: step.result?.reason || null
185
+ }))
186
+ }
187
+ });
188
+ rootState = await getRecruitRoots(client);
189
+ }
190
+
191
+ await runControl.waitIfPaused();
192
+ runControl.throwIfCanceled();
193
+ runControl.setPhase("recruit:cards");
194
+ cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
195
+ timeoutMs: cardTimeoutMs,
196
+ intervalMs: 300
197
+ });
198
+ if (!cardNodeIds.length) {
199
+ throw new Error("No recruit/search candidate cards found for run service");
200
+ }
201
+
202
+ runControl.updateProgress({
203
+ card_count: cardNodeIds.length,
204
+ target_count: limit,
205
+ processed: 0,
206
+ screened: 0,
207
+ detail_opened: 0,
208
+ passed: 0,
209
+ unique_seen: compactInfiniteListState(listState).seen_count,
210
+ scroll_count: 0,
211
+ refresh_rounds: 0,
212
+ refresh_attempts: 0
213
+ });
214
+
215
+ while (results.length < limit) {
216
+ await runControl.waitIfPaused();
217
+ runControl.throwIfCanceled();
218
+ runControl.setPhase("recruit:candidate");
219
+
220
+ const nextCandidateResult = await getNextInfiniteListCandidate({
221
+ client,
222
+ state: listState,
223
+ maxScrolls: listMaxScrolls,
224
+ stableSignatureLimit: listStableSignatureLimit,
225
+ wheelDeltaY: listWheelDeltaY,
226
+ settleMs: listSettleMs,
227
+ fallbackPoint: listFallbackPoint,
228
+ findNodeIds: async () => {
229
+ const currentRootState = await getRecruitRoots(client);
230
+ const currentCardNodeIds = await waitForRecruitCardNodeIds(client, currentRootState.iframe.documentNodeId, {
231
+ timeoutMs: Math.min(cardTimeoutMs, 5000),
232
+ intervalMs: 300
233
+ });
234
+ cardNodeIds = currentCardNodeIds;
235
+ return currentCardNodeIds;
236
+ },
237
+ readCandidate: async (nodeId, { visibleIndex }) => readRecruitCardCandidate(client, nodeId, {
238
+ targetUrl,
239
+ source: "recruit-run-card",
240
+ metadata: {
241
+ run_candidate_index: results.length,
242
+ visible_index: visibleIndex,
243
+ search_params: normalizedSearchParams
244
+ }
245
+ })
246
+ });
247
+ if (!nextCandidateResult.ok) {
248
+ listEndReason = nextCandidateResult.reason || "list_exhausted";
249
+ if (
250
+ nextCandidateResult.end_reached
251
+ && refreshOnEnd
252
+ && results.length < limit
253
+ && refreshRounds < Math.max(0, Number(maxRefreshRounds) || 0)
254
+ ) {
255
+ await runControl.waitIfPaused();
256
+ runControl.throwIfCanceled();
257
+ runControl.setPhase("recruit:refresh");
258
+ refreshRounds += 1;
259
+ const refreshResult = await refreshRecruitSearchAtEnd(client, {
260
+ searchParams: normalizedSearchParams,
261
+ requireCards: true,
262
+ searchTimeoutMs: cardTimeoutMs,
263
+ resetTimeoutMs,
264
+ resetSettleMs: refreshResetSettleMs,
265
+ cityOptionTimeoutMs
266
+ });
267
+ const compactRefresh = compactRefreshAttempt(refreshResult);
268
+ refreshAttempts.push(compactRefresh);
269
+ runControl.checkpoint({
270
+ refresh_round: refreshRounds,
271
+ refresh: compactRefresh
272
+ });
273
+ runControl.updateProgress({
274
+ card_count: refreshResult.card_count || cardNodeIds.length,
275
+ target_count: limit,
276
+ processed: results.length,
277
+ screened: results.length,
278
+ detail_opened: results.filter((item) => item.detail).length,
279
+ passed: results.filter((item) => item.screening.passed).length,
280
+ unique_seen: compactInfiniteListState(listState).seen_count,
281
+ scroll_count: compactInfiniteListState(listState).scroll_count,
282
+ refresh_rounds: refreshRounds,
283
+ refresh_attempts: refreshAttempts.length,
284
+ refresh_method: refreshResult.method || null,
285
+ refresh_forced_recent_viewed: true,
286
+ list_end_reason: listEndReason
287
+ });
288
+ if (refreshResult.ok) {
289
+ rootState = await getRecruitRoots(client);
290
+ cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
291
+ timeoutMs: cardTimeoutMs,
292
+ intervalMs: 300
293
+ });
294
+ resetInfiniteListForRefreshRound(listState, {
295
+ reason: listEndReason,
296
+ round: refreshRounds,
297
+ method: refreshResult.method,
298
+ metadata: {
299
+ card_count: cardNodeIds.length,
300
+ forced_recent_viewed: true
301
+ }
302
+ });
303
+ listEndReason = "";
304
+ continue;
305
+ }
306
+ }
307
+ break;
308
+ }
309
+
310
+ const index = results.length;
311
+ const cardNodeId = nextCandidateResult.item.node_id;
312
+ const candidateKey = nextCandidateResult.item.key;
313
+ const cardCandidate = nextCandidateResult.item.candidate;
314
+
315
+ let screeningCandidate = cardCandidate;
316
+ let detailResult = null;
317
+ if (index < detailCountLimit) {
318
+ await runControl.waitIfPaused();
319
+ runControl.throwIfCanceled();
320
+ runControl.setPhase("recruit:detail");
321
+ networkRecorder.clear();
322
+ const openedDetail = await openRecruitCardDetail(client, cardNodeId);
323
+ const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
324
+ const networkWait = await waitForCvNetworkEvents(
325
+ waitForRecruitDetailNetworkEvents,
326
+ networkRecorder,
327
+ {
328
+ waitPlan,
329
+ minCount: 1,
330
+ requireLoaded: true,
331
+ intervalMs: 120
332
+ }
333
+ );
334
+ detailResult = await extractRecruitDetailCandidate(client, {
335
+ cardCandidate,
336
+ cardNodeId,
337
+ detailState: openedDetail.detail_state,
338
+ networkEvents: networkRecorder.events,
339
+ targetUrl,
340
+ closeDetail: false
341
+ });
342
+ const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
343
+ let source = "network";
344
+ let imageEvidence = null;
345
+ if (parsedNetworkProfileCount > 0) {
346
+ recordCvNetworkHit(cvAcquisitionState, {
347
+ parsedNetworkProfileCount,
348
+ waitResult: networkWait
349
+ });
350
+ } else {
351
+ const captureNodeId = openedDetail.detail_state?.popup?.node_id
352
+ || openedDetail.detail_state?.resumeIframe?.node_id
353
+ || null;
354
+ if (captureNodeId) {
355
+ imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
356
+ padding: 4,
357
+ maxScreenshots: maxImagePages,
358
+ wheelDeltaY: imageWheelDeltaY,
359
+ settleMs: 1200,
360
+ metadata: {
361
+ domain: "recruit",
362
+ capture_mode: "scroll_sequence",
363
+ acquisition_reason: "network_miss_image_fallback",
364
+ run_candidate_index: index,
365
+ candidate_key: candidateKey
366
+ }
367
+ });
368
+ source = "image";
369
+ recordCvImageFallback(cvAcquisitionState, {
370
+ parsedNetworkProfileCount,
371
+ waitResult: networkWait,
372
+ imageEvidence
373
+ });
374
+ } else {
375
+ source = "missing_capture_node";
376
+ recordCvNetworkMiss(cvAcquisitionState, {
377
+ reason: "network_miss_no_capture_node",
378
+ parsedNetworkProfileCount,
379
+ waitResult: networkWait
380
+ });
381
+ }
382
+ }
383
+
384
+ let closeResult = null;
385
+ if (closeDetail) {
386
+ closeResult = await closeRecruitDetail(client);
387
+ }
388
+ detailResult.close_result = closeResult;
389
+ detailResult.image_evidence = imageEvidence;
390
+ detailResult.cv_acquisition = {
391
+ source,
392
+ mode_after: compactCvAcquisitionState(cvAcquisitionState).mode,
393
+ wait_plan: waitPlan,
394
+ network_wait: networkWait,
395
+ parsed_network_profile_count: parsedNetworkProfileCount,
396
+ image_evidence: summarizeImageEvidence(imageEvidence)
397
+ };
398
+ screeningCandidate = detailResult.candidate;
399
+ }
400
+
401
+ await runControl.waitIfPaused();
402
+ runControl.throwIfCanceled();
403
+ runControl.setPhase("recruit:screening");
404
+ const screening = screenCandidate(screeningCandidate, { criteria });
405
+ const compactResult = {
406
+ index,
407
+ candidate_key: candidateKey,
408
+ card_node_id: cardNodeId,
409
+ candidate: compactCandidate(screeningCandidate),
410
+ detail: compactDetail(detailResult),
411
+ screening: compactScreening(screening)
412
+ };
413
+ results.push(compactResult);
414
+ markInfiniteListCandidateProcessed(listState, candidateKey, {
415
+ metadata: {
416
+ result_index: index,
417
+ candidate_id: screeningCandidate.id || null
418
+ }
419
+ });
420
+
421
+ runControl.updateProgress({
422
+ card_count: cardNodeIds.length,
423
+ target_count: limit,
424
+ processed: results.length,
425
+ screened: results.length,
426
+ detail_opened: results.filter((item) => item.detail).length,
427
+ passed: results.filter((item) => item.screening.passed).length,
428
+ unique_seen: compactInfiniteListState(listState).seen_count,
429
+ scroll_count: compactInfiniteListState(listState).scroll_count,
430
+ refresh_rounds: refreshRounds,
431
+ refresh_attempts: refreshAttempts.length,
432
+ list_end_reason: listEndReason || null,
433
+ last_candidate_id: screeningCandidate.id || null,
434
+ last_candidate_key: candidateKey,
435
+ last_score: screening.score
436
+ });
437
+ runControl.checkpoint({
438
+ last_candidate: {
439
+ id: screeningCandidate.id || null,
440
+ key: candidateKey,
441
+ identity: screeningCandidate.identity || {},
442
+ screening: {
443
+ status: screening.status,
444
+ passed: screening.passed,
445
+ score: screening.score
446
+ }
447
+ }
448
+ });
449
+
450
+ if (delayMs > 0) {
451
+ await runControl.sleep(delayMs);
452
+ }
453
+ }
454
+
455
+ runControl.setPhase("recruit:done");
456
+ return {
457
+ domain: "recruit",
458
+ target_url: targetUrl,
459
+ search_params: normalizedSearchParams,
460
+ card_count: cardNodeIds.length,
461
+ candidate_list: compactInfiniteListState(listState),
462
+ list_end_reason: listEndReason || null,
463
+ refresh_rounds: refreshRounds,
464
+ refresh_attempts: refreshAttempts,
465
+ processed: results.length,
466
+ screened: results.length,
467
+ detail_opened: results.filter((item) => item.detail).length,
468
+ passed: results.filter((item) => item.screening.passed).length,
469
+ results
470
+ };
471
+ }
472
+
473
+ export function createRecruitRunService({
474
+ lifecycle,
475
+ idPrefix = "recruit",
476
+ workflow = runRecruitWorkflow
477
+ } = {}) {
478
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix });
479
+
480
+ function startRecruitRun({
481
+ client,
482
+ targetUrl = "",
483
+ criteria = "",
484
+ searchParams = {},
485
+ maxCandidates = 5,
486
+ detailLimit = 1,
487
+ closeDetail = true,
488
+ delayMs = 0,
489
+ cardTimeoutMs = 90000,
490
+ resetBeforeSearch = true,
491
+ resetTimeoutMs = 180000,
492
+ cityOptionTimeoutMs = 30000,
493
+ maxImagePages = 8,
494
+ imageWheelDeltaY = 650,
495
+ cvAcquisitionMode = "unknown",
496
+ listMaxScrolls = 20,
497
+ listStableSignatureLimit = 2,
498
+ listWheelDeltaY = 850,
499
+ listSettleMs = 1200,
500
+ listFallbackPoint = null,
501
+ refreshOnEnd = true,
502
+ maxRefreshRounds = 2,
503
+ refreshResetSettleMs = 5000,
504
+ name = "recruit-domain-run"
505
+ } = {}) {
506
+ if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
507
+ const normalizedSearchParams = normalizeSearchParams(searchParams);
508
+ return manager.startRun({
509
+ name,
510
+ context: {
511
+ domain: "recruit",
512
+ target_url: targetUrl,
513
+ criteria_present: Boolean(criteria),
514
+ search_params: normalizedSearchParams,
515
+ max_candidates: maxCandidates,
516
+ detail_limit: detailLimit,
517
+ close_detail: closeDetail,
518
+ reset_before_search: resetBeforeSearch,
519
+ reset_timeout_ms: resetTimeoutMs,
520
+ city_option_timeout_ms: cityOptionTimeoutMs,
521
+ cv_acquisition_mode: cvAcquisitionMode,
522
+ max_image_pages: maxImagePages,
523
+ image_wheel_delta_y: imageWheelDeltaY,
524
+ list_max_scrolls: listMaxScrolls,
525
+ list_stable_signature_limit: listStableSignatureLimit,
526
+ list_wheel_delta_y: listWheelDeltaY,
527
+ list_settle_ms: listSettleMs,
528
+ list_fallback_point: listFallbackPoint,
529
+ refresh_on_end: refreshOnEnd,
530
+ max_refresh_rounds: maxRefreshRounds,
531
+ refresh_reset_settle_ms: refreshResetSettleMs
532
+ },
533
+ progress: {
534
+ card_count: 0,
535
+ target_count: Math.max(1, Number(maxCandidates) || 1),
536
+ processed: 0,
537
+ screened: 0,
538
+ detail_opened: 0,
539
+ passed: 0
540
+ },
541
+ checkpoint: {},
542
+ task: (runControl) => workflow({
543
+ client,
544
+ targetUrl,
545
+ criteria,
546
+ searchParams: normalizedSearchParams,
547
+ maxCandidates,
548
+ detailLimit,
549
+ closeDetail,
550
+ delayMs,
551
+ cardTimeoutMs,
552
+ resetBeforeSearch,
553
+ resetTimeoutMs,
554
+ cityOptionTimeoutMs,
555
+ maxImagePages,
556
+ imageWheelDeltaY,
557
+ cvAcquisitionMode,
558
+ listMaxScrolls,
559
+ listStableSignatureLimit,
560
+ listWheelDeltaY,
561
+ listSettleMs,
562
+ listFallbackPoint,
563
+ refreshOnEnd,
564
+ maxRefreshRounds,
565
+ refreshResetSettleMs
566
+ }, runControl)
567
+ });
568
+ }
569
+
570
+ return {
571
+ startRecruitRun,
572
+ getRecruitRun: manager.getRun,
573
+ pauseRecruitRun: manager.pauseRun,
574
+ resumeRecruitRun: manager.resumeRun,
575
+ cancelRecruitRun: manager.cancelRun,
576
+ waitForRecruitRun: manager.waitForRun,
577
+ listRecruitRuns: manager.listRuns,
578
+ manager
579
+ };
580
+ }