@reconcrap/boss-recommend-mcp 2.0.55 → 2.0.57

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.
@@ -3,12 +3,13 @@ import {
3
3
  clickPoint,
4
4
  DETERMINISTIC_CLICK_OPTIONS,
5
5
  getFrameDocumentNodeId,
6
- getNodeBox,
7
- getOuterHTML,
8
- pressKey,
9
- querySelectorAll,
10
- sleep
11
- } from "../../core/browser/index.js";
6
+ getNodeBox,
7
+ getOuterHTML,
8
+ pressKey,
9
+ querySelectorAll,
10
+ scrollNodeIntoView,
11
+ sleep
12
+ } from "../../core/browser/index.js";
12
13
  import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
13
14
  import {
14
15
  buildScreeningCandidateFromDetail,
@@ -18,12 +19,14 @@ import {
18
19
  closeBossAccountRightsBlockingPanel,
19
20
  findBossAccountRightsBlockingPanel
20
21
  } from "../common/account-rights-panel.js";
21
- import {
22
- DETAIL_CLOSE_SELECTORS,
23
- DETAIL_NETWORK_PATTERNS,
24
- DETAIL_POPUP_SELECTORS,
25
- DETAIL_RESUME_IFRAME_SELECTORS
26
- } from "./constants.js";
22
+ import {
23
+ DETAIL_CLOSE_SELECTORS,
24
+ DETAIL_NETWORK_PATTERNS,
25
+ DETAIL_POPUP_SELECTORS,
26
+ DETAIL_RESUME_IFRAME_SELECTORS,
27
+ RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS,
28
+ RECOMMEND_AVATAR_PREVIEW_SELECTORS
29
+ } from "./constants.js";
27
30
  import {
28
31
  getRecommendRoots
29
32
  } from "./roots.js";
@@ -93,6 +96,15 @@ export async function closeRecommendBlockingPanels(client, options = {}) {
93
96
  });
94
97
  }
95
98
 
99
+ function looksLikeRecommendAvatarPreviewHtml(html = "") {
100
+ return /\bavatar-preview\b|\bfigure-preview\b/i.test(String(html || ""));
101
+ }
102
+
103
+ function isRecommendAvatarPreviewOpenError(error) {
104
+ return error?.code === "RECOMMEND_AVATAR_PREVIEW_OPENED"
105
+ || /RECOMMEND_AVATAR_PREVIEW_OPENED/i.test(String(error?.message || error || ""));
106
+ }
107
+
96
108
  export async function waitForRecommendDetailNetworkEvents(recorder, {
97
109
  minCount = 1,
98
110
  requireLoaded = true,
@@ -163,10 +175,10 @@ export async function waitForRecommendDetail(client, {
163
175
  return lastState;
164
176
  }
165
177
 
166
- async function readRecommendDetailState(client) {
167
- const rootState = await getRecommendRoots(client);
168
- const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
169
- const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
178
+ async function readRecommendDetailState(client) {
179
+ const rootState = await getRecommendRoots(client);
180
+ const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
181
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
170
182
  return {
171
183
  iframe: rootState.iframe,
172
184
  roots: rootState.roots,
@@ -199,6 +211,46 @@ export async function waitForRecommendDetailClosed(client, {
199
211
  };
200
212
  }
201
213
 
214
+ export async function readRecommendAvatarPreviewState(client) {
215
+ const rootState = await getRecommendRoots(client);
216
+ const topRoot = rootState.rootNodes?.top
217
+ ? [{ name: "top", nodeId: rootState.rootNodes.top }]
218
+ : rootState.roots.filter((root) => root?.name === "top");
219
+ const preview = await findVisibleDetailTarget(client, topRoot, RECOMMEND_AVATAR_PREVIEW_SELECTORS, {
220
+ includeAvatarPreview: true
221
+ });
222
+ return {
223
+ open: Boolean(preview),
224
+ root: rootState.topRoot,
225
+ roots: topRoot,
226
+ preview
227
+ };
228
+ }
229
+
230
+ export async function waitForRecommendAvatarPreviewClosed(client, {
231
+ timeoutMs = 1200,
232
+ intervalMs = 120
233
+ } = {}) {
234
+ const started = Date.now();
235
+ let state = null;
236
+ while (Date.now() - started <= timeoutMs) {
237
+ state = await readRecommendAvatarPreviewState(client);
238
+ if (!state.open) {
239
+ return {
240
+ closed: true,
241
+ elapsed_ms: Date.now() - started,
242
+ state
243
+ };
244
+ }
245
+ await sleep(intervalMs);
246
+ }
247
+ return {
248
+ closed: false,
249
+ elapsed_ms: Date.now() - started,
250
+ state
251
+ };
252
+ }
253
+
202
254
  function compactRect(rect) {
203
255
  if (!rect) return null;
204
256
  return {
@@ -254,18 +306,28 @@ async function verifyRecommendDetailStillOpen(client, {
254
306
  };
255
307
  }
256
308
 
257
- async function findVisibleDetailTarget(client, roots, selectors) {
309
+ async function findVisibleDetailTarget(client, roots, selectors, {
310
+ includeAvatarPreview = false
311
+ } = {}) {
258
312
  for (const root of roots) {
259
313
  if (!root?.nodeId) continue;
260
- for (const selector of selectors) {
261
- const nodeIds = await querySelectorAll(client, root.nodeId, selector);
262
- for (const nodeId of nodeIds) {
263
- try {
264
- const box = await getNodeBox(client, nodeId);
265
- if (box.rect.width > 2 && box.rect.height > 2) {
266
- return {
267
- root: root.name,
268
- root_node_id: root.nodeId,
314
+ for (const selector of selectors) {
315
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
316
+ for (const nodeId of nodeIds) {
317
+ try {
318
+ const box = await getNodeBox(client, nodeId);
319
+ if (box.rect.width > 2 && box.rect.height > 2) {
320
+ if (!includeAvatarPreview) {
321
+ try {
322
+ const html = await getOuterHTML(client, nodeId);
323
+ if (looksLikeRecommendAvatarPreviewHtml(html)) continue;
324
+ } catch {
325
+ // If the node went stale, let the next candidate decide.
326
+ }
327
+ }
328
+ return {
329
+ root: root.name,
330
+ root_node_id: root.nodeId,
269
331
  selector,
270
332
  node_id: nodeId,
271
333
  center: box.center,
@@ -332,7 +394,104 @@ export function isStaleRecommendNodeError(error) {
332
394
 
333
395
  export function isRecommendDetailOpenMissError(error) {
334
396
  const message = String(error?.message || error || "");
335
- return /Candidate detail did not open|no known detail selectors mounted/i.test(message);
397
+ return isRecommendAvatarPreviewOpenError(error)
398
+ || /Candidate detail did not open|no known detail selectors mounted/i.test(message);
399
+ }
400
+
401
+ export function resolveRecommendCardDetailClickPoint(cardBox, {
402
+ attemptIndex = 0
403
+ } = {}) {
404
+ const rect = cardBox?.rect || {};
405
+ const width = Number(rect.width) || 0;
406
+ const height = Number(rect.height) || 0;
407
+ if (width <= 2 || height <= 2) {
408
+ return {
409
+ ...(cardBox?.center || { x: 0, y: 0 }),
410
+ mode: "card-center-fallback",
411
+ reason: "invalid_card_rect"
412
+ };
413
+ }
414
+
415
+ const xFractions = [0.22, 0.50, 0.72];
416
+ const xFraction = xFractions[Math.min(Math.max(0, attemptIndex), xFractions.length - 1)];
417
+ const minOffsetX = Math.min(width - 12, Math.max(110, Math.min(180, width * 0.18)));
418
+ const maxOffsetX = Math.max(minOffsetX, width - Math.min(220, Math.max(90, width * 0.22)));
419
+ const rawOffsetX = width * xFraction;
420
+ const offsetX = clampPointCoordinate(rawOffsetX, minOffsetX, maxOffsetX);
421
+ const offsetY = clampPointCoordinate(height * 0.28, Math.min(34, height / 2), Math.max(36, height - 28));
422
+ return {
423
+ x: rect.x + offsetX,
424
+ y: rect.y + offsetY,
425
+ mode: "card-body-safe-point",
426
+ attempt_index: attemptIndex,
427
+ offset_x: Math.round(offsetX),
428
+ offset_y: Math.round(offsetY)
429
+ };
430
+ }
431
+
432
+ async function clickRecommendCardDetailPoint(client, nodeId, {
433
+ scrollIntoView = true,
434
+ attemptIndex = 0
435
+ } = {}) {
436
+ if (scrollIntoView) {
437
+ try {
438
+ await scrollNodeIntoView(client, nodeId);
439
+ await sleep(150);
440
+ } catch {
441
+ // Recommend list cards are selected from visible nodes; if this CDP
442
+ // helper races the virtual list, let the box lookup/retry decide.
443
+ }
444
+ }
445
+ const box = await getNodeBox(client, nodeId);
446
+ const clickTarget = resolveRecommendCardDetailClickPoint(box, { attemptIndex });
447
+ const clickResult = await clickPoint(client, clickTarget.x, clickTarget.y, DETERMINISTIC_CLICK_OPTIONS);
448
+ return {
449
+ ...box,
450
+ click_target: clickTarget,
451
+ click_result: clickResult
452
+ };
453
+ }
454
+
455
+ async function waitForRecommendDetailOpenOutcome(client, {
456
+ timeoutMs = 10000,
457
+ intervalMs = 250
458
+ } = {}) {
459
+ const started = Date.now();
460
+ let detailState = null;
461
+ let avatarPreview = null;
462
+ while (Date.now() - started <= timeoutMs) {
463
+ detailState = await readRecommendDetailState(client);
464
+ if (detailState?.popup || detailState?.resumeIframe) {
465
+ return {
466
+ kind: "detail",
467
+ elapsed_ms: Date.now() - started,
468
+ detail_state: detailState
469
+ };
470
+ }
471
+ avatarPreview = await readRecommendAvatarPreviewState(client);
472
+ if (avatarPreview.open) {
473
+ return {
474
+ kind: "avatar_preview",
475
+ elapsed_ms: Date.now() - started,
476
+ avatar_preview: avatarPreview
477
+ };
478
+ }
479
+ await sleep(intervalMs);
480
+ }
481
+ return {
482
+ kind: "none",
483
+ elapsed_ms: Date.now() - started,
484
+ detail_state: detailState,
485
+ avatar_preview: avatarPreview
486
+ };
487
+ }
488
+
489
+ function makeRecommendAvatarPreviewOpenedError(outcome, clickAttempts = []) {
490
+ const error = new Error("RECOMMEND_AVATAR_PREVIEW_OPENED: candidate avatar preview opened instead of resume detail");
491
+ error.code = "RECOMMEND_AVATAR_PREVIEW_OPENED";
492
+ error.avatar_preview = outcome?.avatar_preview || null;
493
+ error.click_attempts = clickAttempts;
494
+ return error;
336
495
  }
337
496
 
338
497
  export async function findRecommendCardNodeForCandidateKey(client, {
@@ -365,8 +524,17 @@ export async function findRecommendCardNodeForCandidateKey(client, {
365
524
  };
366
525
  }
367
526
 
368
- const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
369
- lastCardCount = nodeIds.length;
527
+ let nodeIds = [];
528
+ try {
529
+ nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
530
+ } catch (error) {
531
+ lastError = error;
532
+ if (!isStaleRecommendNodeError(error)) throw error;
533
+ rootState = null;
534
+ if (intervalMs > 0) await sleep(intervalMs);
535
+ continue;
536
+ }
537
+ lastCardCount = nodeIds.length;
370
538
  for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
371
539
  const nodeId = nodeIds[visibleIndex];
372
540
  try {
@@ -412,35 +580,71 @@ export async function findRecommendCardNodeForCandidateKey(client, {
412
580
  };
413
581
  }
414
582
 
415
- export async function openRecommendCardDetail(client, cardNodeId, {
416
- timeoutMs = 12000,
417
- scrollIntoView = true
418
- } = {}) {
419
- const started = Date.now();
420
- const clickStarted = Date.now();
421
- const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
422
- const candidateClickMs = Date.now() - clickStarted;
423
- const detailStarted = Date.now();
424
- const detailState = await waitForRecommendDetail(client, { timeoutMs });
425
- const detailOpenMs = Date.now() - detailStarted;
426
- if (!detailState?.popup && !detailState?.resumeIframe) {
427
- throw new Error("Candidate detail did not open or no known detail selectors mounted");
428
- }
429
-
430
- return {
431
- card_box: cardBox,
432
- detail_state: detailState,
433
- timings: {
434
- candidate_click_ms: candidateClickMs,
435
- detail_open_ms: detailOpenMs,
436
- open_total_ms: Date.now() - started
437
- }
438
- };
439
- }
583
+ export async function openRecommendCardDetail(client, cardNodeId, {
584
+ timeoutMs = 12000,
585
+ scrollIntoView = true
586
+ } = {}) {
587
+ const started = Date.now();
588
+ const clickAttempts = [];
589
+ const maxClickAttempts = 3;
590
+ let lastOutcome = null;
591
+ let lastCardBox = null;
592
+ let candidateClickMs = 0;
593
+ let detailOpenMs = 0;
594
+
595
+ for (let attemptIndex = 0; attemptIndex < maxClickAttempts; attemptIndex += 1) {
596
+ const clickStarted = Date.now();
597
+ lastCardBox = await clickRecommendCardDetailPoint(client, cardNodeId, {
598
+ scrollIntoView: attemptIndex === 0 ? scrollIntoView : false,
599
+ attemptIndex
600
+ });
601
+ candidateClickMs += Date.now() - clickStarted;
602
+ const detailStarted = Date.now();
603
+ lastOutcome = await waitForRecommendDetailOpenOutcome(client, {
604
+ timeoutMs: attemptIndex === 0 ? timeoutMs : Math.max(2500, Math.floor(timeoutMs / 3)),
605
+ intervalMs: 250
606
+ });
607
+ detailOpenMs += Date.now() - detailStarted;
608
+ clickAttempts.push({
609
+ attempt: attemptIndex + 1,
610
+ click_target: lastCardBox.click_target,
611
+ click_result: lastCardBox.click_result,
612
+ outcome: lastOutcome.kind,
613
+ elapsed_ms: lastOutcome.elapsed_ms
614
+ });
615
+
616
+ if (lastOutcome.kind === "detail") {
617
+ return {
618
+ card_box: lastCardBox,
619
+ click_attempts: clickAttempts,
620
+ detail_state: lastOutcome.detail_state,
621
+ timings: {
622
+ candidate_click_ms: candidateClickMs,
623
+ detail_open_ms: detailOpenMs,
624
+ open_total_ms: Date.now() - started
625
+ }
626
+ };
627
+ }
628
+
629
+ if (lastOutcome.kind === "avatar_preview") {
630
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2, waitMs: 350 });
631
+ throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
632
+ }
633
+ break;
634
+ }
635
+
636
+ if (lastOutcome?.kind === "avatar_preview") {
637
+ throw makeRecommendAvatarPreviewOpenedError(lastOutcome, clickAttempts);
638
+ }
639
+ const error = new Error("Candidate detail did not open or no known detail selectors mounted");
640
+ error.click_attempts = clickAttempts;
641
+ error.last_open_outcome = lastOutcome;
642
+ throw error;
643
+ }
440
644
 
441
- export async function openRecommendCardDetailWithFreshRetry(client, {
442
- cardNodeId,
443
- candidateKey = "",
645
+ export async function openRecommendCardDetailWithFreshRetry(client, {
646
+ cardNodeId,
647
+ candidateKey = "",
444
648
  cardCandidate = null,
445
649
  rootState = null,
446
650
  targetUrl = "",
@@ -507,12 +711,100 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
507
711
  currentRootState = resolved.root_state || null;
508
712
  }
509
713
  }
510
-
511
- throw new Error("Recommend detail retry exhausted");
512
- }
513
-
514
- export async function closeRecommendDetail(client, {
515
- attemptsLimit = 4,
714
+
715
+ throw new Error("Recommend detail retry exhausted");
716
+ }
717
+
718
+ export async function closeRecommendAvatarPreview(client, {
719
+ attemptsLimit = 2,
720
+ waitMs = 500
721
+ } = {}) {
722
+ const attempts = [];
723
+ for (let index = 0; index < attemptsLimit; index += 1) {
724
+ const state = await readRecommendAvatarPreviewState(client);
725
+ if (!state.open) {
726
+ return {
727
+ closed: true,
728
+ already_closed: true,
729
+ attempts
730
+ };
731
+ }
732
+
733
+ const closeTarget = await findVisibleCloseTarget(client, state.roots, RECOMMEND_AVATAR_PREVIEW_CLOSE_SELECTORS);
734
+ if (closeTarget) {
735
+ try {
736
+ if (closeTarget.center) {
737
+ await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
738
+ } else {
739
+ await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
740
+ }
741
+ attempts.push({
742
+ mode: "avatar-preview-close-selector",
743
+ selector: closeTarget.selector,
744
+ root: closeTarget.root
745
+ });
746
+ } catch (error) {
747
+ attempts.push({
748
+ mode: "avatar-preview-close-selector-error",
749
+ selector: closeTarget.selector,
750
+ root: closeTarget.root,
751
+ error: error?.message || String(error)
752
+ });
753
+ }
754
+ } else {
755
+ await pressEscape(client);
756
+ attempts.push({ mode: "avatar-preview-Escape" });
757
+ }
758
+
759
+ const closed = await waitForRecommendAvatarPreviewClosed(client, {
760
+ timeoutMs: waitMs,
761
+ intervalMs: 100
762
+ });
763
+ attempts.push({
764
+ mode: "wait-avatar-preview-closed",
765
+ closed: closed.closed,
766
+ elapsed_ms: closed.elapsed_ms
767
+ });
768
+ if (closed.closed) {
769
+ return {
770
+ closed: true,
771
+ already_closed: false,
772
+ attempts
773
+ };
774
+ }
775
+
776
+ await pressEscape(client);
777
+ attempts.push({ mode: "avatar-preview-Escape-fallback" });
778
+ const closedAfterEscape = await waitForRecommendAvatarPreviewClosed(client, {
779
+ timeoutMs: waitMs,
780
+ intervalMs: 100
781
+ });
782
+ attempts.push({
783
+ mode: "wait-avatar-preview-closed-after-escape",
784
+ closed: closedAfterEscape.closed,
785
+ elapsed_ms: closedAfterEscape.elapsed_ms
786
+ });
787
+ if (closedAfterEscape.closed) {
788
+ return {
789
+ closed: true,
790
+ already_closed: false,
791
+ attempts
792
+ };
793
+ }
794
+ }
795
+
796
+ const state = await readRecommendAvatarPreviewState(client);
797
+ return {
798
+ closed: !state.open,
799
+ already_closed: false,
800
+ reason: state.open ? "avatar_preview_still_visible_after_close_attempts" : null,
801
+ attempts,
802
+ state
803
+ };
804
+ }
805
+
806
+ export async function closeRecommendDetail(client, {
807
+ attemptsLimit = 4,
516
808
  closeWaitMs = 5000,
517
809
  escapeWaitMs = 3500
518
810
  } = {}) {
@@ -47,6 +47,7 @@ import {
47
47
  } from "../../core/screening/index.js";
48
48
  import {
49
49
  closeRecommendBlockingPanels,
50
+ closeRecommendAvatarPreview,
50
51
  closeRecommendDetail,
51
52
  createRecommendDetailNetworkRecorder,
52
53
  extractRecommendDetailCandidate,
@@ -506,6 +507,16 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
506
507
  if (Array.isArray(error.recommend_detail_open_attempts)) {
507
508
  result.recommend_detail_open_attempts = error.recommend_detail_open_attempts;
508
509
  }
510
+ if (Array.isArray(error.click_attempts)) {
511
+ result.click_attempts = error.click_attempts;
512
+ }
513
+ if (error.avatar_preview) {
514
+ result.avatar_preview = {
515
+ open: Boolean(error.avatar_preview.open),
516
+ selector: error.avatar_preview.preview?.selector || null,
517
+ rect: error.avatar_preview.preview?.rect || null
518
+ };
519
+ }
509
520
  return result;
510
521
  }
511
522
 
@@ -907,6 +918,7 @@ export async function runRecommendWorkflow({
907
918
 
908
919
  runControl.setPhase("recommend:cleanup");
909
920
  await closeRecommendDetail(client, { attemptsLimit: 2 });
921
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 });
910
922
  await closeRecommendBlockingPanelsForRun("cleanup");
911
923
 
912
924
  await runControl.waitIfPaused();
@@ -1247,6 +1259,7 @@ export async function runRecommendWorkflow({
1247
1259
  timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
1248
1260
  checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1249
1261
  await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1262
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1250
1263
  await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1251
1264
  await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
1252
1265
  forceRecentNotView: true
@@ -1299,6 +1312,7 @@ export async function runRecommendWorkflow({
1299
1312
  timings.detail_recovery_trigger = compactRecoverableDetailError(error);
1300
1313
  checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
1301
1314
  await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1315
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1302
1316
  await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1303
1317
  await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
1304
1318
  forceRecentNotView: true
@@ -1309,6 +1323,7 @@ export async function runRecommendWorkflow({
1309
1323
  detailResult = null;
1310
1324
  timings.detail_recovered_error = compactRecoverableDetailError(error);
1311
1325
  await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
1326
+ await closeRecommendAvatarPreview(client, { attemptsLimit: 2 }).catch(() => null);
1312
1327
  await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
1313
1328
  }
1314
1329
  }
@@ -1146,6 +1146,7 @@ export function createRecruitRunService({
1146
1146
  const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1147
1147
 
1148
1148
  function startRecruitRun({
1149
+ runId = "",
1149
1150
  client,
1150
1151
  targetUrl = "",
1151
1152
  criteria = "",
@@ -1189,6 +1190,7 @@ export function createRecruitRunService({
1189
1190
  });
1190
1191
  const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
1191
1192
  return manager.startRun({
1193
+ runId,
1192
1194
  name,
1193
1195
  context: {
1194
1196
  domain: "recruit",
package/src/index.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  pauseBossChatRunTool,
22
22
  prepareBossChatRunTool,
23
23
  resumeBossChatRunTool,
24
+ startBossChatDetachedRunTool,
24
25
  startBossChatRunTool
25
26
  } from "./chat-mcp.js";
26
27
  import {
@@ -34,6 +35,7 @@ import {
34
35
  pauseRecruitPipelineRunTool,
35
36
  resumeRecruitPipelineRunTool,
36
37
  runRecruitPipelineTool,
38
+ startRecruitPipelineDetachedRunTool,
37
39
  startRecruitPipelineRunTool,
38
40
  validateRecruitPipelineArgs
39
41
  } from "./recruit-mcp.js";
@@ -135,6 +137,8 @@ const recommendTargetUrl = "https://www.zhipin.com/web/chat/recommend";
135
137
  let runPipelineImpl = null;
136
138
  let runSelfHealImpl = null;
137
139
  let spawnProcessImpl = spawn;
140
+ let forceChatInProcForTests = false;
141
+ let forceRecruitInProcForTests = false;
138
142
  const TERMINAL_RUN_STATES = new Set([RUN_STATE_COMPLETED, RUN_STATE_FAILED, RUN_STATE_CANCELED]);
139
143
 
140
144
  async function getRunPipelineImpl() {
@@ -185,6 +189,20 @@ function shouldStartRecommendDetached({ workspaceRoot = "" } = {}) {
185
189
  return isLikelyAgentRuntime({ workspaceRoot });
186
190
  }
187
191
 
192
+ function shouldStartChatDetached({ workspaceRoot = "" } = {}) {
193
+ if (forceChatInProcForTests) return false;
194
+ if (normalizeText(process.env.BOSS_CHAT_CDP_INPROC || "") === "1") return false;
195
+ if (normalizeText(process.env.BOSS_CHAT_CDP_DETACHED || "") === "1") return true;
196
+ return isLikelyAgentRuntime({ workspaceRoot });
197
+ }
198
+
199
+ function shouldStartRecruitDetached({ workspaceRoot = "" } = {}) {
200
+ if (forceRecruitInProcForTests) return false;
201
+ if (normalizeText(process.env.BOSS_RECRUIT_CDP_INPROC || "") === "1") return false;
202
+ if (normalizeText(process.env.BOSS_RECRUIT_CDP_DETACHED || "") === "1") return true;
203
+ return isLikelyAgentRuntime({ workspaceRoot });
204
+ }
205
+
188
206
  function isUnlimitedTargetCountToken(value) {
189
207
  const token = normalizeText(value).toLowerCase();
190
208
  if (!token) return false;
@@ -2468,6 +2486,9 @@ async function handleBossChatPrepareRunTool({ workspaceRoot, args }) {
2468
2486
  }
2469
2487
 
2470
2488
  async function handleBossChatStartRunTool({ workspaceRoot, args }) {
2489
+ if (shouldStartChatDetached({ workspaceRoot })) {
2490
+ return startBossChatDetachedRunTool({ workspaceRoot, args });
2491
+ }
2471
2492
  return startBossChatRunTool({ workspaceRoot, args });
2472
2493
  }
2473
2494
 
@@ -2618,9 +2639,15 @@ async function handleRequest(message, workspaceRoot) {
2618
2639
  } else if (toolName === TOOL_BOSS_CHAT_CANCEL_RUN) {
2619
2640
  payload = await handleBossChatCancelRunTool({ workspaceRoot, args });
2620
2641
  } else if (toolName === TOOL_RUN_RECRUIT_PIPELINE) {
2621
- payload = await runRecruitPipelineTool({ workspaceRoot, args });
2642
+ payload = normalizeText(args.execution_mode || "").toLowerCase() === "sync"
2643
+ ? await runRecruitPipelineTool({ workspaceRoot, args })
2644
+ : shouldStartRecruitDetached({ workspaceRoot })
2645
+ ? await startRecruitPipelineDetachedRunTool({ workspaceRoot, args })
2646
+ : await runRecruitPipelineTool({ workspaceRoot, args });
2622
2647
  } else if (toolName === TOOL_START_RECRUIT_PIPELINE_RUN) {
2623
- payload = await startRecruitPipelineRunTool({ workspaceRoot, args });
2648
+ payload = shouldStartRecruitDetached({ workspaceRoot })
2649
+ ? await startRecruitPipelineDetachedRunTool({ workspaceRoot, args })
2650
+ : await startRecruitPipelineRunTool({ workspaceRoot, args });
2624
2651
  } else if (toolName === TOOL_GET_RECRUIT_PIPELINE_RUN) {
2625
2652
  payload = getRecruitPipelineRunTool({ workspaceRoot, args });
2626
2653
  } else if (toolName === TOOL_CANCEL_RECRUIT_PIPELINE_RUN) {
@@ -2781,24 +2808,30 @@ export const __testables = {
2781
2808
  __resetRecommendMcpStateForTests();
2782
2809
  },
2783
2810
  setChatMcpConnectorForTests(nextImpl) {
2811
+ forceChatInProcForTests = typeof nextImpl === "function";
2784
2812
  __setChatMcpConnectorForTests(nextImpl);
2785
2813
  },
2786
2814
  setChatMcpJobReaderForTests(nextImpl) {
2787
2815
  __setChatMcpJobReaderForTests(nextImpl);
2788
2816
  },
2789
2817
  setChatMcpWorkflowForTests(nextImpl) {
2818
+ forceChatInProcForTests = typeof nextImpl === "function";
2790
2819
  __setChatMcpWorkflowForTests(nextImpl);
2791
2820
  },
2792
2821
  resetChatMcpStateForTests() {
2822
+ forceChatInProcForTests = false;
2793
2823
  __resetChatMcpStateForTests();
2794
2824
  },
2795
2825
  setRecruitMcpConnectorForTests(nextImpl) {
2826
+ forceRecruitInProcForTests = typeof nextImpl === "function";
2796
2827
  __setRecruitMcpConnectorForTests(nextImpl);
2797
2828
  },
2798
2829
  setRecruitMcpWorkflowForTests(nextImpl) {
2830
+ forceRecruitInProcForTests = typeof nextImpl === "function";
2799
2831
  __setRecruitMcpWorkflowForTests(nextImpl);
2800
2832
  },
2801
2833
  resetRecruitMcpStateForTests() {
2834
+ forceRecruitInProcForTests = false;
2802
2835
  __resetRecruitMcpStateForTests();
2803
2836
  }
2804
2837
  };