@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.
@@ -206,11 +206,52 @@ function parseDateLike(value) {
206
206
  return normalized;
207
207
  }
208
208
 
209
+ function isLikelySalaryLine(value = "") {
210
+ const normalized = normalizeText(value);
211
+ return Boolean(
212
+ /^(?:面议|薪资面议)$/i.test(normalized)
213
+ || /^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?$/.test(normalized)
214
+ || /^\d+\s*-\s*\d+\s*元\s*\/\s*天$/.test(normalized)
215
+ );
216
+ }
217
+
218
+ function isLikelyStatusLine(value = "") {
219
+ const normalized = normalizeText(value);
220
+ return Boolean(
221
+ !normalized
222
+ || /^沟通|^收藏|^查看|^不合适/.test(normalized)
223
+ || /^(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/.test(normalized)
224
+ );
225
+ }
226
+
227
+ function stripLeadingSalaryToken(value = "") {
228
+ return normalizeText(value)
229
+ .replace(/^(?:面议|薪资面议)\s+/i, "")
230
+ .replace(/^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?\s+/, "")
231
+ .replace(/^\d+\s*-\s*\d+\s*元\s*\/\s*天\s+/, "")
232
+ .trim();
233
+ }
234
+
235
+ function stripTrailingStatusToken(value = "") {
236
+ return normalizeText(value)
237
+ .replace(/\s*(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/u, "")
238
+ .trim();
239
+ }
240
+
241
+ function cleanInferredNameLine(value = "") {
242
+ const withoutSalary = stripLeadingSalaryToken(value);
243
+ const withoutStatus = stripTrailingStatusToken(withoutSalary);
244
+ return withoutStatus && !isLikelyStatusLine(withoutStatus) && !isLikelySalaryLine(withoutStatus)
245
+ ? withoutStatus
246
+ : "";
247
+ }
248
+
209
249
  function firstUsefulLine(lines) {
210
- return lines.find((line) => {
211
- const normalized = normalizeText(line);
212
- return normalized && !/^沟通|^收藏|^查看|^不合适/.test(normalized);
213
- }) || null;
250
+ for (const line of lines) {
251
+ const cleaned = cleanInferredNameLine(line);
252
+ if (cleaned) return cleaned;
253
+ }
254
+ return null;
214
255
  }
215
256
 
216
257
  function parseNetworkBodyText(networkBody = {}) {
@@ -248,6 +289,72 @@ function tryExtractJsonObject(text) {
248
289
  return null;
249
290
  }
250
291
 
292
+ function extractBalancedJsonAt(text = "", startIndex = 0) {
293
+ const source = String(text || "");
294
+ const start = source.indexOf("{", Math.max(0, Number(startIndex) || 0));
295
+ if (start < 0) return "";
296
+ let depth = 0;
297
+ let inString = false;
298
+ let quote = "";
299
+ let escaped = false;
300
+ for (let index = start; index < source.length; index += 1) {
301
+ const char = source[index];
302
+ if (inString) {
303
+ if (escaped) {
304
+ escaped = false;
305
+ } else if (char === "\\") {
306
+ escaped = true;
307
+ } else if (char === quote) {
308
+ inString = false;
309
+ quote = "";
310
+ }
311
+ continue;
312
+ }
313
+ if (char === "\"" || char === "'") {
314
+ inString = true;
315
+ quote = char;
316
+ continue;
317
+ }
318
+ if (char === "{") depth += 1;
319
+ if (char === "}") {
320
+ depth -= 1;
321
+ if (depth === 0) return source.slice(start, index + 1);
322
+ }
323
+ }
324
+ return "";
325
+ }
326
+
327
+ function tryParseEmbeddedJsonObjects(text = "") {
328
+ const source = decodeHtmlEntities(String(text || ""));
329
+ const objects = [];
330
+ const anchors = [
331
+ "__INITIAL_STATE__",
332
+ "__NEXT_DATA__",
333
+ "geekDetailInfo",
334
+ "geekDetail",
335
+ "geekBaseInfo",
336
+ "geekEduExpList",
337
+ "geekWorkExpList",
338
+ "resume"
339
+ ];
340
+ for (const anchor of anchors) {
341
+ let searchIndex = 0;
342
+ while (searchIndex >= 0 && searchIndex < source.length) {
343
+ const anchorIndex = source.indexOf(anchor, searchIndex);
344
+ if (anchorIndex < 0) break;
345
+ const windowStart = Math.max(0, anchorIndex - 4000);
346
+ const braceIndex = source.lastIndexOf("{", anchorIndex);
347
+ if (braceIndex >= windowStart) {
348
+ const jsonText = extractBalancedJsonAt(source, braceIndex);
349
+ const parsed = tryParseJson(jsonText);
350
+ if (parsed && typeof parsed === "object") objects.push(parsed);
351
+ }
352
+ searchIndex = anchorIndex + anchor.length;
353
+ }
354
+ }
355
+ return objects;
356
+ }
357
+
251
358
  function flattenChatMessageContent(content) {
252
359
  if (typeof content === "string") return content;
253
360
  if (Array.isArray(content)) {
@@ -351,6 +458,65 @@ function pickFirst(...values) {
351
458
  return "";
352
459
  }
353
460
 
461
+ function isPlainObject(value) {
462
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
463
+ }
464
+
465
+ function isBossGeekDetailShape(value) {
466
+ if (!isPlainObject(value)) return false;
467
+ return Boolean(
468
+ isPlainObject(value.geekBaseInfo)
469
+ || value.geekName
470
+ || value.geekAdvantage
471
+ || Array.isArray(value.geekEduExpList)
472
+ || Array.isArray(value.geekEducationList)
473
+ || Array.isArray(value.geekWorkExpList)
474
+ || Array.isArray(value.geekProjExpList)
475
+ || Array.isArray(value.geekCertificationList)
476
+ || Array.isArray(value.geekSkillList)
477
+ || isPlainObject(value.highestEduExp)
478
+ );
479
+ }
480
+
481
+ function isBossChatProfileShape(value) {
482
+ if (!isPlainObject(value)) return false;
483
+ return Boolean(
484
+ (value.name || value.encryptGeekId || value.uid)
485
+ && (
486
+ Array.isArray(value.eduExpList)
487
+ || Array.isArray(value.workExpList)
488
+ || value.school
489
+ || value.major
490
+ || value.lastCompany
491
+ || value.positionName
492
+ )
493
+ );
494
+ }
495
+
496
+ function collectObjects(root, {
497
+ maxObjects = 500,
498
+ maxDepth = 8
499
+ } = {}) {
500
+ if (!root || typeof root !== "object") return [];
501
+ const queue = [{ value: root, depth: 0 }];
502
+ const seen = new WeakSet();
503
+ const objects = [];
504
+ while (queue.length && objects.length < maxObjects) {
505
+ const { value, depth } = queue.shift();
506
+ if (!value || typeof value !== "object" || seen.has(value)) continue;
507
+ seen.add(value);
508
+ if (isPlainObject(value)) objects.push(value);
509
+ if (depth >= maxDepth) continue;
510
+ const children = Array.isArray(value) ? value : Object.values(value);
511
+ for (const child of children) {
512
+ if (child && typeof child === "object") {
513
+ queue.push({ value: child, depth: depth + 1 });
514
+ }
515
+ }
516
+ }
517
+ return objects;
518
+ }
519
+
354
520
  function joinRange(start, end, fallback = "") {
355
521
  const left = parseDateLike(start);
356
522
  const right = parseDateLike(end);
@@ -420,8 +586,8 @@ function formatEducation(item = {}, index = 0) {
420
586
  ];
421
587
  return [
422
588
  `${index + 1}. ${[
423
- pickFirst(item.school),
424
- pickFirst(item.major),
589
+ pickFirst(item.school, item.schoolName),
590
+ pickFirst(item.major, item.majorName),
425
591
  pickFirst(item.degreeName, item.degree),
426
592
  period
427
593
  ].filter(Boolean).join(" / ")}`,
@@ -466,16 +632,28 @@ function resolveBossGeekDetail(payload = {}) {
466
632
  const candidates = [
467
633
  { sourceKey: "geekDetailInfo", detail: payload?.zpData?.geekDetailInfo },
468
634
  { sourceKey: "geekDetail", detail: payload?.zpData?.geekDetail },
635
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.geekDetailInfo },
636
+ { sourceKey: "geekDetail", detail: payload?.zpData?.data?.geekDetail },
637
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.detailInfo },
638
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.resumeDetail },
639
+ { sourceKey: "geekDetailInfo", detail: payload?.zpData?.data },
640
+ { sourceKey: "geekDetailInfo", detail: payload?.data?.geekDetailInfo },
641
+ { sourceKey: "geekDetail", detail: payload?.data?.geekDetail },
642
+ { sourceKey: "geekDetailInfo", detail: payload?.data?.detailInfo },
643
+ { sourceKey: "geekDetailInfo", detail: payload?.data?.resumeDetail },
644
+ { sourceKey: "geekDetailInfo", detail: payload?.data },
469
645
  { sourceKey: "geekDetailInfo", detail: payload?.geekDetailInfo },
470
- { sourceKey: "geekDetail", detail: payload?.geekDetail }
646
+ { sourceKey: "geekDetail", detail: payload?.geekDetail },
647
+ { sourceKey: "geekDetailInfo", detail: payload }
471
648
  ];
472
- const found = candidates.find((item) => item.detail && typeof item.detail === "object");
649
+ const found = candidates.find((item) => isBossGeekDetailShape(item.detail));
473
650
  return found || { sourceKey: "", detail: null };
474
651
  }
475
652
 
476
653
  function extractBossChatGeekInfo(payload = {}) {
477
- const data = payload?.zpData?.data;
654
+ const data = payload?.zpData?.data || payload?.data || payload?.zpData?.geekInfo || payload?.geekInfo;
478
655
  if (!data || typeof data !== "object") return null;
656
+ if (!isBossChatProfileShape(data)) return null;
479
657
  const educationList = normalizeList(data.eduExpList);
480
658
  const workList = normalizeList(data.workExpList);
481
659
  const firstEducation = educationList[0] || {};
@@ -544,7 +722,13 @@ function extractBossChatGeekInfo(payload = {}) {
544
722
  }
545
723
 
546
724
  function extractBossChatHistoryResume(payload = {}) {
547
- const messages = normalizeList(payload?.zpData?.messages);
725
+ const messages = normalizeList(payload?.zpData?.messages).length
726
+ ? normalizeList(payload?.zpData?.messages)
727
+ : normalizeList(payload?.messages).length
728
+ ? normalizeList(payload?.messages)
729
+ : normalizeList(payload?.data?.messages).length
730
+ ? normalizeList(payload?.data?.messages)
731
+ : normalizeList(payload?.zpData?.data?.messages);
548
732
  const resumes = messages
549
733
  .map((message) => message?.body?.resume)
550
734
  .filter((resume) => resume && typeof resume === "object");
@@ -606,12 +790,56 @@ function extractBossChatHistoryResume(payload = {}) {
606
790
  };
607
791
  }
608
792
 
793
+ function extractBossProfileRecursively(payload = {}) {
794
+ for (const object of collectObjects(payload)) {
795
+ if (isBossGeekDetailShape(object)) {
796
+ const profile = extractBossGeekDetailInfo({ geekDetailInfo: object });
797
+ if (profile?.text || profile?.identity?.name) {
798
+ return {
799
+ ...profile,
800
+ source_keys: {
801
+ ...(profile.source_keys || {}),
802
+ recursive_profile_match: true
803
+ }
804
+ };
805
+ }
806
+ }
807
+ if (isBossChatProfileShape(object)) {
808
+ const profile = extractBossChatGeekInfo({ zpData: { data: object } });
809
+ if (profile?.text || profile?.identity?.name) {
810
+ return {
811
+ ...profile,
812
+ source_keys: {
813
+ ...(profile.source_keys || {}),
814
+ recursive_profile_match: true
815
+ }
816
+ };
817
+ }
818
+ }
819
+ if (isPlainObject(object.resume)) {
820
+ const profile = extractBossChatHistoryResume({ zpData: { messages: [{ body: { resume: object.resume } }] } });
821
+ if (profile?.text || profile?.identity?.name) {
822
+ return {
823
+ ...profile,
824
+ source_keys: {
825
+ ...(profile.source_keys || {}),
826
+ recursive_profile_match: true
827
+ }
828
+ };
829
+ }
830
+ }
831
+ }
832
+ return null;
833
+ }
834
+
609
835
  function extractBossGeekDetailInfo(payload = {}) {
610
836
  const { sourceKey, detail } = resolveBossGeekDetail(payload);
611
837
  if (!detail || typeof detail !== "object") return null;
612
838
 
613
- const base = detail.geekBaseInfo || {};
614
- const educationList = normalizeList(detail.geekEduExpList);
839
+ const base = detail.geekBaseInfo || detail.baseInfo || detail.base || {};
840
+ const educationList = normalizeList(detail.geekEduExpList).length
841
+ ? normalizeList(detail.geekEduExpList)
842
+ : normalizeList(detail.geekEducationList);
615
843
  const firstEducation = educationList[0] || detail.highestEduExp || {};
616
844
  const workList = normalizeList(detail.geekWorkExpList);
617
845
  const firstWork = workList[0] || {};
@@ -625,6 +853,8 @@ function extractBossGeekDetailInfo(payload = {}) {
625
853
  const normalizedExpectationList = expectationList.length ? expectationList : expectationFallback;
626
854
  const certifications = normalizeList(detail.geekCertificationList);
627
855
  const skillTags = [
856
+ ...normalizeTagList(detail.geekSkillList),
857
+ ...normalizeTagList(detail.skillList),
628
858
  ...normalizeTagList(detail.blueGeekSkills),
629
859
  ...normalizeTagList(base.userHighlightList),
630
860
  ...normalizeTagList(base.userDescHighlightList),
@@ -633,6 +863,7 @@ function extractBossGeekDetailInfo(payload = {}) {
633
863
  ...normalizeTagList(detail.professionalSkill)
634
864
  ];
635
865
  const summaryParts = [
866
+ pickFirst(detail.geekAdvantage),
636
867
  pickFirst(base.userDescription),
637
868
  pickFirst(base.userDesc),
638
869
  pickFirst(base.workEduDesc),
@@ -640,7 +871,7 @@ function extractBossGeekDetailInfo(payload = {}) {
640
871
  ].filter(Boolean);
641
872
  const sections = {
642
873
  base: [
643
- base.name ? `姓名:${normalizeText(base.name)}` : "",
874
+ pickFirst(base.name, detail.geekName, detail.name) ? `姓名:${pickFirst(base.name, detail.geekName, detail.name)}` : "",
644
875
  normalizeGenderValue(base.gender) ? `性别:${normalizeGenderValue(base.gender)}` : "",
645
876
  pickFirst(base.ageDesc, base.age) ? `年龄:${pickFirst(base.ageDesc, base.age)}` : "",
646
877
  pickFirst(base.degreeCategory) ? `最高学历:${pickFirst(base.degreeCategory)}` : "",
@@ -669,11 +900,11 @@ function extractBossGeekDetailInfo(payload = {}) {
669
900
 
670
901
  return {
671
902
  identity: {
672
- name: pickFirst(base.name),
903
+ name: pickFirst(base.name, detail.geekName, detail.name),
673
904
  current_position: pickFirst(firstWork.positionName, firstWork.positionTitle, firstWork.position),
674
- current_company: pickFirst(firstWork.formattedCompany, firstWork.company),
675
- school: pickFirst(firstEducation.school),
676
- major: pickFirst(firstEducation.major),
905
+ current_company: pickFirst(firstWork.formattedCompany, firstWork.company, firstWork.brandName),
906
+ school: pickFirst(firstEducation.school, firstEducation.schoolName),
907
+ major: pickFirst(firstEducation.major, firstEducation.majorName),
677
908
  degree: pickFirst(base.degreeCategory, firstEducation.degreeName, firstEducation.degree),
678
909
  years_experience: parseYearsExperience(pickFirst(base.workYearDesc, base.workYearsDesc)) ?? null,
679
910
  age: parseAge(pickFirst(base.ageDesc, base.age)) ?? null,
@@ -703,24 +934,68 @@ function extractBossGeekDetailInfo(payload = {}) {
703
934
 
704
935
  export function extractBossProfileFromNetworkBody(networkBody = {}) {
705
936
  const text = parseNetworkBodyText(networkBody);
706
- const parsed = tryParseJson(text);
707
- if (!parsed) {
937
+ const parsedObjects = [
938
+ tryParseJson(text),
939
+ ...tryParseEmbeddedJsonObjects(text)
940
+ ].filter((item) => item && typeof item === "object");
941
+ if (!parsedObjects.length) {
942
+ const htmlText = /<html|<body|<div|<section|<script/i.test(text) ? htmlToText(text) : "";
943
+ if (htmlText && htmlText.length > 80) {
944
+ const candidate = normalizeCandidateProfile({
945
+ domain: "recommend",
946
+ source: "network-html-fallback",
947
+ text: htmlText
948
+ });
949
+ return {
950
+ ok: true,
951
+ url: networkBody.url || null,
952
+ status: networkBody.status ?? null,
953
+ mimeType: networkBody.mimeType || null,
954
+ text_length: text.length,
955
+ profile: {
956
+ identity: candidate.identity,
957
+ tags: candidate.tags,
958
+ sections: { html_text: [htmlText] },
959
+ text: htmlText,
960
+ source_keys: {
961
+ network_html_text: true,
962
+ html_text_length: htmlText.length
963
+ }
964
+ }
965
+ };
966
+ }
708
967
  return {
709
968
  ok: false,
710
969
  error: "NETWORK_BODY_NOT_JSON",
711
970
  text_length: text.length
712
971
  };
713
972
  }
714
- const profile = extractBossGeekDetailInfo(parsed)
715
- || extractBossChatGeekInfo(parsed)
716
- || extractBossChatHistoryResume(parsed);
973
+ let profile = null;
974
+ let parsed = parsedObjects[0];
975
+ for (const candidateObject of parsedObjects) {
976
+ profile = extractBossGeekDetailInfo(candidateObject)
977
+ || extractBossChatGeekInfo(candidateObject)
978
+ || extractBossChatHistoryResume(candidateObject)
979
+ || extractBossProfileRecursively(candidateObject);
980
+ if (profile) {
981
+ parsed = candidateObject;
982
+ break;
983
+ }
984
+ }
717
985
  if (!profile) {
986
+ const encryptedPayload = parsedObjects.find((item) => (
987
+ normalizeText(item?.zpData?.encryptGeekDetailInfo || item?.encryptGeekDetailInfo || "")
988
+ ));
718
989
  return {
719
990
  ok: false,
720
- error: "BOSS_GEEK_DETAIL_INFO_NOT_FOUND",
991
+ error: encryptedPayload ? "BOSS_GEEK_DETAIL_INFO_ENCRYPTED" : "BOSS_GEEK_DETAIL_INFO_NOT_FOUND",
721
992
  text_length: text.length,
722
- top_level_keys: Object.keys(parsed).slice(0, 30),
723
- zpData_keys: Object.keys(parsed?.zpData || {}).slice(0, 50)
993
+ parsed_object_count: parsedObjects.length,
994
+ top_level_keys: Object.keys(parsed || {}).slice(0, 30),
995
+ zpData_keys: Object.keys(parsed?.zpData || {}).slice(0, 50),
996
+ data_keys: Object.keys(parsed?.data || parsed?.zpData?.data || {}).slice(0, 50),
997
+ encrypted_resume: Boolean(encryptedPayload),
998
+ encrypted_resume_length: normalizeText(encryptedPayload?.zpData?.encryptGeekDetailInfo || encryptedPayload?.encryptGeekDetailInfo || "").length
724
999
  };
725
1000
  }
726
1001
  return {
@@ -834,7 +1109,8 @@ export function normalizeCandidateProfile(input = {}) {
834
1109
  || attrs.href
835
1110
  || ""
836
1111
  ) || null;
837
- const inferredName = normalizeText(input.identity?.name || input.name || firstUsefulLine(lines) || "") || null;
1112
+ const explicitName = cleanInferredNameLine(input.identity?.name || input.name || "");
1113
+ const inferredName = explicitName || firstUsefulLine(lines) || null;
838
1114
  const fullText = collectTextParts({
839
1115
  ...input,
840
1116
  text: rawText,
@@ -1046,6 +1322,8 @@ export function createFailedLlmScreeningResult(error) {
1046
1322
  decision_cot: "",
1047
1323
  reasoning_content: "",
1048
1324
  raw_model_output: "",
1325
+ image_input_count: Number(error?.image_input_count) || 0,
1326
+ image_inputs: Array.isArray(error?.image_inputs) ? error.image_inputs : [],
1049
1327
  error: error?.message || String(error || "unknown"),
1050
1328
  screened_at: nowIso()
1051
1329
  };
@@ -1134,6 +1412,7 @@ export async function callScreeningLlm({
1134
1412
  const payload = {
1135
1413
  model,
1136
1414
  temperature: 0.1,
1415
+ max_tokens: Math.max(1, Number(config.maxTokens || config.llmMaxTokens) || 64),
1137
1416
  messages: buildScreeningLlmMessages({
1138
1417
  candidate,
1139
1418
  criteria,
@@ -1143,7 +1422,7 @@ export async function callScreeningLlm({
1143
1422
  applyChatCompletionThinking(payload, {
1144
1423
  baseUrl,
1145
1424
  model,
1146
- thinkingLevel: config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort
1425
+ thinkingLevel: config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort || "off"
1147
1426
  });
1148
1427
 
1149
1428
  const controller = new AbortController();
@@ -1208,6 +1487,10 @@ export async function callScreeningLlm({
1208
1487
  image_inputs: summarizeLlmImageInputs(imageInputs),
1209
1488
  screened_at: nowIso()
1210
1489
  };
1490
+ } catch (error) {
1491
+ error.image_input_count = imageInputs.length;
1492
+ error.image_inputs = summarizeLlmImageInputs(imageInputs);
1493
+ throw error;
1211
1494
  } finally {
1212
1495
  clearTimeout(timer);
1213
1496
  }
@@ -5,6 +5,7 @@ import {
5
5
  querySelectorAll,
6
6
  sleep
7
7
  } from "../../core/browser/index.js";
8
+ import { mergeBossCandidateCardFields } from "../../core/boss-cards/index.js";
8
9
  import {
9
10
  htmlToText,
10
11
  normalizeCandidateProfile,
@@ -24,6 +25,12 @@ function firstCandidateId(attributes = {}) {
24
25
  ) || null;
25
26
  }
26
27
 
28
+ function mergeChatCardFields(candidate, outerHTML = "") {
29
+ return mergeBossCandidateCardFields(candidate, outerHTML, {
30
+ metadataKey: "chat_card_fields"
31
+ });
32
+ }
33
+
27
34
  export async function findChatCandidateNodeIds(client, rootNodeId, {
28
35
  selectors = CHAT_CARD_SELECTORS
29
36
  } = {}) {
@@ -97,7 +104,7 @@ export async function readChatCardCandidate(client, cardNodeId, {
97
104
  getAttributesMap(client, cardNodeId),
98
105
  getOuterHTML(client, cardNodeId)
99
106
  ]);
100
- return normalizeCandidateProfile({
107
+ const candidate = normalizeCandidateProfile({
101
108
  domain: "chat",
102
109
  source,
103
110
  id: firstCandidateId(attributes),
@@ -110,6 +117,7 @@ export async function readChatCardCandidate(client, cardNodeId, {
110
117
  ...metadata
111
118
  }
112
119
  });
120
+ return mergeChatCardFields(candidate, outerHTML);
113
121
  }
114
122
 
115
123
  export async function readFirstChatCardCandidate(client, rootNodeId, options = {}) {
@@ -1273,10 +1273,10 @@ export async function extractChatProfileCandidate(client, {
1273
1273
  resumeHtml: providedResumeHtml = null,
1274
1274
  networkEvents = [],
1275
1275
  targetUrl = "",
1276
- closeResume = true
1276
+ closeResume = true,
1277
+ networkParseRetryMs = 1800,
1278
+ networkParseIntervalMs = 250
1277
1279
  } = {}) {
1278
- await sleep(1000);
1279
- const networkBodies = await readChatProfileNetworkBodies(client, networkEvents);
1280
1280
  let resumeHtml = providedResumeHtml || null;
1281
1281
  if (!resumeHtml) {
1282
1282
  try {
@@ -1292,21 +1292,30 @@ export async function extractChatProfileCandidate(client, {
1292
1292
  resumeHtml.resumeIframeText
1293
1293
  ].filter(Boolean).join("\n\n");
1294
1294
 
1295
- const detailCandidateResult = buildScreeningCandidateFromDetail({
1296
- domain: "chat",
1297
- source: "chat-live-cdp-profile",
1298
- cardCandidate,
1299
- detailText,
1300
- networkBodies,
1301
- metadata: {
1302
- target_url: targetUrl,
1303
- card_node_id: cardNodeId,
1304
- resume_popup_selector: resumeState?.popup?.selector || null,
1305
- resume_content_selector: resumeState?.content?.selector || null,
1306
- resume_iframe_selector: resumeState?.resumeIframe?.selector || null,
1307
- resume_iframe_document_node_id: resumeHtml.resumeIframeDocumentNodeId
1308
- }
1309
- });
1295
+ const parseStarted = Date.now();
1296
+ let networkBodies = [];
1297
+ let detailCandidateResult = null;
1298
+ do {
1299
+ networkBodies = await readChatProfileNetworkBodies(client, networkEvents);
1300
+ detailCandidateResult = buildScreeningCandidateFromDetail({
1301
+ domain: "chat",
1302
+ source: "chat-live-cdp-profile",
1303
+ cardCandidate,
1304
+ detailText,
1305
+ networkBodies,
1306
+ metadata: {
1307
+ target_url: targetUrl,
1308
+ card_node_id: cardNodeId,
1309
+ resume_popup_selector: resumeState?.popup?.selector || null,
1310
+ resume_content_selector: resumeState?.content?.selector || null,
1311
+ resume_iframe_selector: resumeState?.resumeIframe?.selector || null,
1312
+ resume_iframe_document_node_id: resumeHtml.resumeIframeDocumentNodeId
1313
+ }
1314
+ });
1315
+ if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
1316
+ if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
1317
+ await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
1318
+ } while (true);
1310
1319
 
1311
1320
  let closeResult = null;
1312
1321
  if (closeResume) {
@@ -1317,6 +1326,8 @@ export async function extractChatProfileCandidate(client, {
1317
1326
  candidate: detailCandidateResult.candidate,
1318
1327
  parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
1319
1328
  network_bodies: networkBodies,
1329
+ network_parse_retry_elapsed_ms: Date.now() - parseStarted,
1330
+ network_event_count: networkEvents.length,
1320
1331
  detail: {
1321
1332
  popup_text: resumeHtml.popupText,
1322
1333
  content_text: resumeHtml.contentText,