@reconcrap/boss-recommend-mcp 2.0.7 → 2.0.9

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.
@@ -4,9 +4,16 @@ import {
4
4
  querySelectorAll,
5
5
  sleep
6
6
  } from "../../core/browser/index.js";
7
+ import { mergeBossCandidateCardFields } from "../../core/boss-cards/index.js";
7
8
  import { normalizeCandidateFromHtml } from "../../core/screening/index.js";
8
9
  import { RECRUIT_CARD_SELECTOR } from "./constants.js";
9
10
 
11
+ function mergeRecruitCardFields(candidate, outerHTML = "") {
12
+ return mergeBossCandidateCardFields(candidate, outerHTML, {
13
+ metadataKey: "search_card_fields"
14
+ });
15
+ }
16
+
10
17
  export async function findRecruitCardNodeIds(client, frameNodeId, {
11
18
  selector = RECRUIT_CARD_SELECTOR
12
19
  } = {}) {
@@ -37,7 +44,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
37
44
  getAttributesMap(client, cardNodeId),
38
45
  getOuterHTML(client, cardNodeId)
39
46
  ]);
40
- return normalizeCandidateFromHtml({
47
+ const candidate = normalizeCandidateFromHtml({
41
48
  domain: "recruit",
42
49
  source,
43
50
  html: outerHTML,
@@ -48,6 +55,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
48
55
  ...metadata
49
56
  }
50
57
  });
58
+ return mergeRecruitCardFields(candidate, outerHTML);
51
59
  }
52
60
 
53
61
  export async function readFirstRecruitCardCandidate(client, frameNodeId, options = {}) {
@@ -213,17 +213,22 @@ export async function waitForRecruitDetailContent(client, {
213
213
  export async function openRecruitCardDetail(client, cardNodeId, {
214
214
  timeoutMs = 12000
215
215
  } = {}) {
216
+ const openedStarted = Date.now();
216
217
  const attempts = [];
218
+ const clickStarted = Date.now();
217
219
  const cardBox = await clickNodeCenter(client, cardNodeId, {
218
220
  scrollIntoView: true
219
221
  });
222
+ let candidateClickMs = Date.now() - clickStarted;
220
223
  attempts.push({
221
224
  mode: "card-center",
222
225
  center: cardBox.center
223
226
  });
227
+ const detailStarted = Date.now();
224
228
  let detailState = await waitForRecruitDetail(client, { timeoutMs });
225
229
 
226
230
  if (!detailState?.popup && !detailState?.resumeIframe) {
231
+ const fallbackClickStarted = Date.now();
227
232
  const leftTitlePoint = {
228
233
  x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
229
234
  y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
@@ -232,6 +237,7 @@ export async function openRecruitCardDetail(client, cardNodeId, {
232
237
  clickCount: 2,
233
238
  delayMs: 120
234
239
  });
240
+ candidateClickMs += Date.now() - fallbackClickStarted;
235
241
  attempts.push({
236
242
  mode: "card-left-title-double-click",
237
243
  center: leftTitlePoint
@@ -248,7 +254,12 @@ export async function openRecruitCardDetail(client, cardNodeId, {
248
254
  return {
249
255
  card_box: cardBox,
250
256
  open_attempts: attempts,
251
- detail_state: detailState
257
+ detail_state: detailState,
258
+ timings: {
259
+ candidate_click_ms: candidateClickMs,
260
+ detail_open_ms: Date.now() - detailStarted,
261
+ open_total_ms: Date.now() - openedStarted
262
+ }
252
263
  };
253
264
  }
254
265
 
@@ -368,31 +379,40 @@ export async function extractRecruitDetailCandidate(client, {
368
379
  detailHtml: providedDetailHtml = null,
369
380
  networkEvents = [],
370
381
  targetUrl = "",
371
- closeDetail = true
382
+ closeDetail = true,
383
+ networkParseRetryMs = 1800,
384
+ networkParseIntervalMs = 250
372
385
  } = {}) {
373
- await sleep(1000);
374
- const networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
375
386
  const detailHtml = providedDetailHtml || await readRecruitDetailHtml(client, detailState);
376
387
  const detailText = [
377
388
  detailHtml.popupText,
378
389
  detailHtml.resumeText
379
390
  ].filter(Boolean).join("\n\n");
380
391
 
381
- const detailCandidateResult = buildScreeningCandidateFromDetail({
382
- domain: "recruit",
383
- cardCandidate,
384
- detailText,
385
- networkBodies,
386
- metadata: {
387
- target_url: targetUrl,
388
- card_node_id: cardNodeId,
389
- detail_popup_selector: detailState?.popup?.selector || null,
390
- detail_popup_root: detailState?.popup?.root || null,
391
- resume_iframe_selector: detailState?.resumeIframe?.selector || null,
392
- resume_iframe_root: detailState?.resumeIframe?.root || null,
393
- resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
394
- }
395
- });
392
+ const parseStarted = Date.now();
393
+ let networkBodies = [];
394
+ let detailCandidateResult = null;
395
+ do {
396
+ networkBodies = await readRecruitDetailNetworkBodies(client, networkEvents);
397
+ detailCandidateResult = buildScreeningCandidateFromDetail({
398
+ domain: "recruit",
399
+ cardCandidate,
400
+ detailText,
401
+ networkBodies,
402
+ metadata: {
403
+ target_url: targetUrl,
404
+ card_node_id: cardNodeId,
405
+ detail_popup_selector: detailState?.popup?.selector || null,
406
+ detail_popup_root: detailState?.popup?.root || null,
407
+ resume_iframe_selector: detailState?.resumeIframe?.selector || null,
408
+ resume_iframe_root: detailState?.resumeIframe?.root || null,
409
+ resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId
410
+ }
411
+ });
412
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
413
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
414
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
415
+ } while (true);
396
416
 
397
417
  let closeResult = null;
398
418
  if (closeDetail) {
@@ -403,6 +423,8 @@ export async function extractRecruitDetailCandidate(client, {
403
423
  candidate: detailCandidateResult.candidate,
404
424
  parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
405
425
  network_bodies: networkBodies,
426
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
427
+ network_event_count: networkEvents.length,
406
428
  detail: {
407
429
  popup_text: detailHtml.popupText,
408
430
  resume_text: detailHtml.resumeText,
@@ -1,4 +1,9 @@
1
1
  import { createRunLifecycleManager } from "../../core/run/index.js";
2
+ import {
3
+ addTiming,
4
+ imageEvidenceFilePath,
5
+ measureTiming
6
+ } from "../../core/run/timing.js";
2
7
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
3
8
  import {
4
9
  compactCvAcquisitionState,
@@ -150,7 +155,8 @@ export async function runRecruitWorkflow({
150
155
  llmConfig = null,
151
156
  llmTimeoutMs = 120000,
152
157
  llmImageLimit = 8,
153
- llmImageDetail = "high"
158
+ llmImageDetail = "high",
159
+ imageOutputDir = ""
154
160
  } = {}, runControl) {
155
161
  if (!client) throw new Error("runRecruitWorkflow requires a guarded CDP client");
156
162
  const normalizedSearchParams = normalizeSearchParams(searchParams);
@@ -258,12 +264,14 @@ export async function runRecruitWorkflow({
258
264
  });
259
265
 
260
266
  while (results.length < limit) {
267
+ const candidateStarted = Date.now();
268
+ const timings = {};
261
269
  await runControl.waitIfPaused();
262
270
  runControl.throwIfCanceled();
263
271
  runControl.setPhase("recruit:candidate");
264
272
  rootState = await ensureRecruitViewport(rootState, "candidate_loop");
265
273
 
266
- const nextCandidateResult = await getNextInfiniteListCandidate({
274
+ const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
267
275
  client,
268
276
  state: listState,
269
277
  maxScrolls: listMaxScrolls,
@@ -291,7 +299,7 @@ export async function runRecruitWorkflow({
291
299
  search_params: normalizedSearchParams
292
300
  }
293
301
  })
294
- });
302
+ }));
295
303
  if (!nextCandidateResult.ok) {
296
304
  listEndReason = nextCandidateResult.reason || "list_exhausted";
297
305
  if (
@@ -373,8 +381,10 @@ export async function runRecruitWorkflow({
373
381
  rootState = await ensureRecruitViewport(rootState, "detail");
374
382
  networkRecorder.clear();
375
383
  const openedDetail = await openRecruitCardDetail(client, cardNodeId);
384
+ addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
385
+ addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
376
386
  const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
377
- const networkWait = await waitForCvNetworkEvents(
387
+ const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
378
388
  waitForRecruitDetailNetworkEvents,
379
389
  networkRecorder,
380
390
  {
@@ -383,15 +393,21 @@ export async function runRecruitWorkflow({
383
393
  requireLoaded: true,
384
394
  intervalMs: 120
385
395
  }
386
- );
396
+ ));
397
+ if (networkWait?.elapsed_ms != null) {
398
+ timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
399
+ }
387
400
  detailResult = await extractRecruitDetailCandidate(client, {
388
401
  cardCandidate,
389
402
  cardNodeId,
390
403
  detailState: openedDetail.detail_state,
391
404
  networkEvents: networkRecorder.events,
392
405
  targetUrl,
393
- closeDetail: false
406
+ closeDetail: false,
407
+ networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
408
+ networkParseIntervalMs: 250
394
409
  });
410
+ addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
395
411
  const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
396
412
  let source = "network";
397
413
  let imageEvidence = null;
@@ -405,11 +421,25 @@ export async function runRecruitWorkflow({
405
421
  || openedDetail.detail_state?.resumeIframe?.node_id
406
422
  || null;
407
423
  if (captureNodeId) {
408
- imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
424
+ imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
425
+ filePath: imageEvidenceFilePath({
426
+ imageOutputDir,
427
+ domain: "recruit",
428
+ runId: runControl?.runId,
429
+ index,
430
+ extension: "jpg"
431
+ }),
432
+ format: "jpeg",
433
+ quality: 72,
434
+ optimize: true,
435
+ resizeMaxWidth: 1100,
436
+ captureViewport: true,
409
437
  padding: 4,
410
438
  maxScreenshots: maxImagePages,
411
439
  wheelDeltaY: imageWheelDeltaY,
412
- settleMs: 1200,
440
+ settleMs: 350,
441
+ duplicateStopCount: 1,
442
+ skipDuplicateScreenshots: true,
413
443
  metadata: {
414
444
  domain: "recruit",
415
445
  capture_mode: "scroll_sequence",
@@ -417,7 +447,7 @@ export async function runRecruitWorkflow({
417
447
  run_candidate_index: index,
418
448
  candidate_key: candidateKey
419
449
  }
420
- });
450
+ }));
421
451
  source = "image";
422
452
  recordCvImageFallback(cvAcquisitionState, {
423
453
  parsedNetworkProfileCount,
@@ -436,7 +466,7 @@ export async function runRecruitWorkflow({
436
466
 
437
467
  let closeResult = null;
438
468
  if (closeDetail) {
439
- closeResult = await closeRecruitDetail(client);
469
+ closeResult = await measureTiming(timings, "close_detail_ms", () => closeRecruitDetail(client));
440
470
  }
441
471
  detailResult.close_result = closeResult;
442
472
  detailResult.image_evidence = imageEvidence;
@@ -460,7 +490,10 @@ export async function runRecruitWorkflow({
460
490
  llmResult = createMissingLlmConfigResult();
461
491
  } else {
462
492
  try {
463
- llmResult = await callScreeningLlm({
493
+ const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
494
+ ? "vision_model_ms"
495
+ : "text_model_ms";
496
+ llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
464
497
  candidate: screeningCandidate,
465
498
  criteria,
466
499
  config: llmConfig,
@@ -468,7 +501,7 @@ export async function runRecruitWorkflow({
468
501
  imageEvidence: detailResult?.image_evidence || null,
469
502
  maxImages: llmImageLimit,
470
503
  imageDetail: llmImageDetail
471
- });
504
+ }));
472
505
  } catch (error) {
473
506
  llmResult = createFailedLlmScreeningResult(error);
474
507
  }
@@ -478,6 +511,7 @@ export async function runRecruitWorkflow({
478
511
  const screening = useLlmScreening
479
512
  ? llmResultToScreening(llmResult, screeningCandidate)
480
513
  : screenCandidate(screeningCandidate, { criteria });
514
+ timings.total_ms = Date.now() - candidateStarted;
481
515
  const compactResult = {
482
516
  index,
483
517
  candidate_key: candidateKey,
@@ -485,7 +519,8 @@ export async function runRecruitWorkflow({
485
519
  candidate: compactCandidate(screeningCandidate),
486
520
  detail: compactDetail(detailResult),
487
521
  llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
488
- screening: compactScreening(screening)
522
+ screening: compactScreening(screening),
523
+ timings
489
524
  };
490
525
  results.push(compactResult);
491
526
  markInfiniteListCandidateProcessed(listState, candidateKey, {
@@ -514,6 +549,7 @@ export async function runRecruitWorkflow({
514
549
  last_candidate_key: candidateKey,
515
550
  last_score: screening.score
516
551
  });
552
+ const checkpointStarted = Date.now();
517
553
  runControl.checkpoint({
518
554
  results,
519
555
  last_candidate: {
@@ -528,9 +564,13 @@ export async function runRecruitWorkflow({
528
564
  llm_screening: compactScreeningLlmResult(llmResult)
529
565
  }
530
566
  });
567
+ addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
531
568
 
532
569
  if (delayMs > 0) {
570
+ const sleepStarted = Date.now();
533
571
  await runControl.sleep(delayMs);
572
+ addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
573
+ compactResult.timings.total_ms = Date.now() - candidateStarted;
534
574
  }
535
575
  }
536
576
 
@@ -593,6 +633,7 @@ export function createRecruitRunService({
593
633
  llmTimeoutMs = 120000,
594
634
  llmImageLimit = 8,
595
635
  llmImageDetail = "high",
636
+ imageOutputDir = "",
596
637
  name = "recruit-domain-run"
597
638
  } = {}) {
598
639
  if (!client) throw new Error("startRecruitRun requires a guarded CDP client");
@@ -628,7 +669,8 @@ export function createRecruitRunService({
628
669
  llm_configured: Boolean(llmConfig),
629
670
  llm_timeout_ms: llmTimeoutMs,
630
671
  llm_image_limit: llmImageLimit,
631
- llm_image_detail: llmImageDetail
672
+ llm_image_detail: llmImageDetail,
673
+ image_output_dir: imageOutputDir || ""
632
674
  },
633
675
  progress: {
634
676
  card_count: 0,
@@ -668,7 +710,8 @@ export function createRecruitRunService({
668
710
  llmConfig,
669
711
  llmTimeoutMs,
670
712
  llmImageLimit,
671
- llmImageDetail
713
+ llmImageDetail,
714
+ imageOutputDir
672
715
  }, runControl)
673
716
  });
674
717
  }
package/src/index.js CHANGED
@@ -572,7 +572,7 @@ function createRunInputSchema() {
572
572
  },
573
573
  llm_image_detail: {
574
574
  type: "string",
575
- description: "可选,图片输入 detail,默认 high"
575
+ description: "可选,图片输入 detail,默认 low"
576
576
  },
577
577
  delay_ms: {
578
578
  type: "integer",
@@ -746,7 +746,7 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
746
746
  },
747
747
  llm_image_detail: {
748
748
  type: "string",
749
- description: "可选,图片输入 detail,默认 high"
749
+ description: "可选,图片输入 detail,默认 low"
750
750
  },
751
751
  online_resume_button_timeout_ms: {
752
752
  type: "integer",
@@ -1045,9 +1045,18 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1045
1045
  llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
1046
1046
  ...configResolution.config
1047
1047
  } : null,
1048
- llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
1049
- llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
1050
- llmImageDetail: normalizeText(args.llm_image_detail) || "high",
1048
+ llmTimeoutMs: parsePositiveInteger(
1049
+ args.llm_timeout_ms,
1050
+ parsePositiveInteger(configResolution?.config?.llmTimeoutMs || configResolution?.config?.timeoutMs, slowLive ? 180000 : 120000)
1051
+ ),
1052
+ llmImageLimit: parsePositiveInteger(
1053
+ args.llm_image_limit,
1054
+ parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
1055
+ ),
1056
+ llmImageDetail: normalizeText(
1057
+ args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
1058
+ ) || "low",
1059
+ imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
1051
1060
  name: "mcp-recommend-pipeline-run",
1052
1061
  parsed
1053
1062
  };
@@ -449,7 +449,7 @@ export function createRecruitPipelineInputSchema() {
449
449
  },
450
450
  llm_image_detail: {
451
451
  type: "string",
452
- description: "可选,图片输入 detail,默认 high"
452
+ description: "可选,图片输入 detail,默认 low"
453
453
  },
454
454
  delay_ms: {
455
455
  type: "integer",
@@ -834,9 +834,18 @@ function getRunOptions(args, parsed, session, configResolution = null) {
834
834
  llmConfig: screeningMode === "llm" && configResolution?.ok ? {
835
835
  ...configResolution.config
836
836
  } : null,
837
- llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
838
- llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
839
- llmImageDetail: normalizeText(args.llm_image_detail) || "high",
837
+ llmTimeoutMs: parsePositiveInteger(
838
+ args.llm_timeout_ms,
839
+ parsePositiveInteger(configResolution?.config?.llmTimeoutMs || configResolution?.config?.timeoutMs, slowLive ? 180000 : 120000)
840
+ ),
841
+ llmImageLimit: parsePositiveInteger(
842
+ args.llm_image_limit,
843
+ parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
844
+ ),
845
+ llmImageDetail: normalizeText(
846
+ args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
847
+ ) || "low",
848
+ imageOutputDir: resolveBossConfiguredOutputDir("", getRecruitRunsDir()),
840
849
  name: "mcp-recruit-pipeline-run"
841
850
  };
842
851
  }