@reconcrap/boss-recommend-mcp 2.0.46 → 2.0.48

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.46",
3
+ "version": "2.0.48",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -29,6 +29,7 @@ export const FORBIDDEN_CDP_DOMAINS = new Set(["Runtime"]);
29
29
  const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com|login\.zhipin\.com)/i;
30
30
  const BOSS_LOGIN_TEXT_PATTERN = /扫码登录|验证码登录|密码登录|登录后|请登录|登录BOSS直聘|Boss登录|BOSS登录/i;
31
31
  const CHROME_DEBUG_UNAVAILABLE_PATTERN = /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|connect|socket hang up/i;
32
+ const CDP_CLOSED_TRANSPORT_PATTERN = /WebSocket is not open|readyState\s+\d+\s+\(CLOSED\)|ECONNRESET|socket hang up|Target closed|Session closed|Connection closed/i;
32
33
  const BOSS_LOGIN_DOM_SELECTORS = [
33
34
  ".login-box",
34
35
  ".login-form",
@@ -872,34 +873,143 @@ export async function connectToChromeTargetOrOpen({
872
873
  }
873
874
  }
874
875
 
875
- export function createGuardedCdpClient(client, { methodLog = [] } = {}) {
876
- return new Proxy(client, {
877
- get(target, property, receiver) {
876
+ export function isClosedCdpTransportError(error) {
877
+ return CDP_CLOSED_TRANSPORT_PATTERN.test(String(error?.message || error || ""));
878
+ }
879
+
880
+ function cloneCdpParams(params = {}) {
881
+ if (!params || typeof params !== "object" || typeof params === "function") return params;
882
+ try {
883
+ return JSON.parse(JSON.stringify(params));
884
+ } catch {
885
+ return { ...params };
886
+ }
887
+ }
888
+
889
+ function shouldReplayCdpSetupCall(domain, method) {
890
+ return method === "enable"
891
+ || (domain === "Network" && method === "setCacheDisabled")
892
+ || (domain === "Page" && method === "bringToFront");
893
+ }
894
+
895
+ export function createGuardedCdpClient(client, { methodLog = [], reconnect = null } = {}) {
896
+ let currentClient = client;
897
+ let reconnectInFlight = null;
898
+ const setupCalls = [];
899
+ const eventSubscriptions = [];
900
+
901
+ async function replaySessionSetup(nextClient) {
902
+ for (const call of setupCalls) {
903
+ const fn = nextClient?.[call.domain]?.[call.method];
904
+ if (typeof fn === "function") {
905
+ await fn.call(nextClient[call.domain], cloneCdpParams(call.params));
906
+ }
907
+ }
908
+ for (const subscription of eventSubscriptions) {
909
+ const fn = nextClient?.[subscription.domain]?.[subscription.event];
910
+ if (typeof fn === "function") {
911
+ fn.call(nextClient[subscription.domain], subscription.listener);
912
+ }
913
+ }
914
+ }
915
+
916
+ async function reconnectClient() {
917
+ if (typeof reconnect !== "function") return null;
918
+ if (!reconnectInFlight) {
919
+ reconnectInFlight = Promise.resolve()
920
+ .then(() => reconnect())
921
+ .then(async (nextClient) => {
922
+ if (!nextClient) throw new Error("CDP reconnect returned no client");
923
+ currentClient = nextClient;
924
+ await replaySessionSetup(nextClient);
925
+ return nextClient;
926
+ })
927
+ .finally(() => {
928
+ reconnectInFlight = null;
929
+ });
930
+ }
931
+ return reconnectInFlight;
932
+ }
933
+
934
+ async function invokeWithReconnect({
935
+ methodNameForLog,
936
+ invoke,
937
+ retryable = true
938
+ }) {
939
+ recordMethod(methodLog, methodNameForLog);
940
+ try {
941
+ return await invoke(currentClient);
942
+ } catch (error) {
943
+ if (!retryable || !isClosedCdpTransportError(error) || typeof reconnect !== "function") {
944
+ throw error;
945
+ }
946
+ await reconnectClient();
947
+ recordMethod(methodLog, `${methodNameForLog}:retry_after_reconnect`);
948
+ return invoke(currentClient);
949
+ }
950
+ }
951
+
952
+ return new Proxy({}, {
953
+ get(_target, property, receiver) {
878
954
  if (property === "send") {
879
955
  return async (method, params = {}) => {
880
956
  if (isForbiddenMethod(method)) {
881
957
  throw new Error(`Forbidden CDP method blocked: ${method}`);
882
958
  }
883
- recordMethod(methodLog, method);
884
- return target.send(method, params);
959
+ return invokeWithReconnect({
960
+ methodNameForLog: method,
961
+ invoke: (activeClient) => activeClient.send(method, params)
962
+ });
885
963
  };
886
964
  }
887
965
 
888
- const value = Reflect.get(target, property, receiver);
966
+ if (property === "close") {
967
+ return async () => currentClient?.close?.();
968
+ }
969
+
970
+ if (property === "__rawClient") return currentClient;
971
+
972
+ const value = Reflect.get(currentClient, property, receiver);
889
973
  if (!value || typeof value !== "object") return value;
890
974
 
891
- return new Proxy(value, {
892
- get(domainTarget, method, domainReceiver) {
975
+ return new Proxy({}, {
976
+ get(_domainTarget, method, domainReceiver) {
977
+ const domainTarget = Reflect.get(currentClient, property, receiver);
893
978
  const domainValue = Reflect.get(domainTarget, method, domainReceiver);
894
979
  if (typeof domainValue !== "function") return domainValue;
895
980
 
896
- return async (params = {}) => {
981
+ return (params = {}) => {
897
982
  const fullMethod = methodName(property, method);
898
983
  if (isForbiddenMethod(fullMethod)) {
899
984
  throw new Error(`Forbidden CDP method blocked: ${fullMethod}`);
900
985
  }
901
- recordMethod(methodLog, fullMethod);
902
- return domainValue.call(domainTarget, params);
986
+ if (typeof params === "function") {
987
+ eventSubscriptions.push({
988
+ domain: property,
989
+ event: method,
990
+ listener: params
991
+ });
992
+ recordMethod(methodLog, fullMethod);
993
+ return domainValue.call(domainTarget, params);
994
+ }
995
+ if (shouldReplayCdpSetupCall(property, method)) {
996
+ setupCalls.push({
997
+ domain: property,
998
+ method,
999
+ params: cloneCdpParams(params)
1000
+ });
1001
+ }
1002
+ return invokeWithReconnect({
1003
+ methodNameForLog: fullMethod,
1004
+ invoke: (activeClient) => {
1005
+ const activeDomain = activeClient?.[property];
1006
+ const activeMethod = activeDomain?.[method];
1007
+ if (typeof activeMethod !== "function") {
1008
+ throw new Error(`CDP method is unavailable after reconnect: ${fullMethod}`);
1009
+ }
1010
+ return activeMethod.call(activeDomain, params);
1011
+ }
1012
+ });
903
1013
  };
904
1014
  }
905
1015
  });
@@ -928,14 +1038,37 @@ export async function connectToChromeTarget({
928
1038
  throw new Error(`No matching Chrome target found on ${host}:${port}.\nAvailable targets:\n${urls}`);
929
1039
  }
930
1040
 
931
- const rawClient = await CDP({ host, port, target });
1041
+ let rawClient = await CDP({ host, port, target });
1042
+ let activeTarget = target;
932
1043
  const methodLog = [];
933
- const client = createGuardedCdpClient(rawClient, { methodLog });
1044
+ const client = createGuardedCdpClient(rawClient, {
1045
+ methodLog,
1046
+ reconnect: async () => {
1047
+ const latestTargets = await listChromeTargets({ host, port });
1048
+ const nextTarget = activeTarget?.id
1049
+ ? latestTargets.find((item) => item?.id === activeTarget.id)
1050
+ : latestTargets.find(matcher);
1051
+ if (!nextTarget) {
1052
+ const urls = latestTargets.map((item) => item.url).filter(Boolean).join("\n");
1053
+ throw new Error(`No matching Chrome target found while reconnecting to ${host}:${port}.\nAvailable targets:\n${urls}`);
1054
+ }
1055
+ try {
1056
+ await rawClient.close();
1057
+ } catch {}
1058
+ rawClient = await CDP({ host, port, target: nextTarget });
1059
+ activeTarget = nextTarget;
1060
+ return rawClient;
1061
+ }
1062
+ });
934
1063
 
935
1064
  return {
936
1065
  client,
937
- rawClient,
938
- target,
1066
+ get rawClient() {
1067
+ return rawClient;
1068
+ },
1069
+ get target() {
1070
+ return activeTarget;
1071
+ },
939
1072
  methodLog,
940
1073
  async close() {
941
1074
  await rawClient.close();
@@ -189,6 +189,44 @@ function normalizeBlockText(input) {
189
189
  return String(input ?? "").trim();
190
190
  }
191
191
 
192
+ function normalizeReasoningKey(input) {
193
+ return normalizeBlockText(input).replace(/\s+/g, " ");
194
+ }
195
+
196
+ function collapseRepeatedReasoningText(input) {
197
+ const text = normalizeBlockText(input);
198
+ if (!text) return "";
199
+ const chunks = text.split(/\n{2,}/).map(normalizeBlockText).filter(Boolean);
200
+ if (chunks.length >= 2 && chunks.length % 2 === 0) {
201
+ const midpoint = chunks.length / 2;
202
+ const first = chunks.slice(0, midpoint);
203
+ const second = chunks.slice(midpoint);
204
+ if (normalizeReasoningKey(first.join("\n\n")) === normalizeReasoningKey(second.join("\n\n"))) {
205
+ return first.join("\n\n");
206
+ }
207
+ }
208
+
209
+ const compacted = normalizeReasoningKey(text);
210
+ if (compacted.length < 160) return text;
211
+ const midpoint = Math.floor(compacted.length / 2);
212
+ for (let offset = -32; offset <= 32; offset += 1) {
213
+ const split = midpoint + offset;
214
+ if (split <= 80 || split >= compacted.length - 80) continue;
215
+ const first = compacted.slice(0, split).trim();
216
+ const second = compacted.slice(split).trim();
217
+ if (first && first === second) return first;
218
+ }
219
+ return text;
220
+ }
221
+
222
+ function firstReasoningText(lines) {
223
+ for (const line of lines) {
224
+ const cleaned = collapseRepeatedReasoningText(line);
225
+ if (cleaned) return cleaned;
226
+ }
227
+ return "";
228
+ }
229
+
192
230
  function compact(input) {
193
231
  return normalizeText(input).toLowerCase();
194
232
  }
@@ -472,7 +510,9 @@ function flattenChatMessageContent(content) {
472
510
 
473
511
  function collectLlmReasoningText(choice = {}) {
474
512
  const message = choice?.message || {};
475
- return [
513
+ const seen = new Set();
514
+ const unique = [];
515
+ for (const item of [
476
516
  message.reasoning_content,
477
517
  message.provider_specific_fields?.reasoning_content,
478
518
  message.reasoning,
@@ -489,7 +529,14 @@ function collectLlmReasoningText(choice = {}) {
489
529
  choice.provider_specific_fields?.cot,
490
530
  choice.chain_of_thought,
491
531
  choice.provider_specific_fields?.chain_of_thought
492
- ].map(flattenChatMessageContent).map(normalizeBlockText).filter(Boolean).join("\n\n");
532
+ ]) {
533
+ const text = collapseRepeatedReasoningText(flattenChatMessageContent(item));
534
+ const key = normalizeReasoningKey(text);
535
+ if (!key || seen.has(key)) continue;
536
+ seen.add(key);
537
+ unique.push(text);
538
+ }
539
+ return collapseRepeatedReasoningText(unique.join("\n\n"));
493
540
  }
494
541
 
495
542
  function mimeTypeForImagePath(filePath) {
@@ -1621,7 +1668,7 @@ async function callScreeningLlmWithProvider({
1621
1668
  const evidence = Array.isArray(parsed?.evidence)
1622
1669
  ? parsed.evidence.map(normalizeText).filter(Boolean)
1623
1670
  : [];
1624
- const decisionCot = firstUsefulLine([
1671
+ const decisionCot = firstReasoningText([
1625
1672
  parsed?.cot,
1626
1673
  parsed?.decision_cot,
1627
1674
  parsed?.reasoning,
@@ -310,12 +310,17 @@ export async function readRecommendDetailHtml(client, detailState) {
310
310
  };
311
311
  }
312
312
 
313
- export function isStaleRecommendNodeError(error) {
314
- const message = String(error?.message || error || "");
315
- return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
316
- }
317
-
318
- export async function findRecommendCardNodeForCandidateKey(client, {
313
+ export function isStaleRecommendNodeError(error) {
314
+ const message = String(error?.message || error || "");
315
+ return /Could not find node with given id|No node with given id|Node is detached|Cannot find node/i.test(message);
316
+ }
317
+
318
+ export function isRecommendDetailOpenMissError(error) {
319
+ const message = String(error?.message || error || "");
320
+ return /Candidate detail did not open|no known detail selectors mounted/i.test(message);
321
+ }
322
+
323
+ export async function findRecommendCardNodeForCandidateKey(client, {
319
324
  candidateKey = "",
320
325
  rootState = null,
321
326
  targetUrl = "",
@@ -448,18 +453,20 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
448
453
  card_candidate: currentCandidate,
449
454
  retry_attempts: attempts
450
455
  };
451
- } catch (error) {
452
- const stale = isStaleRecommendNodeError(error);
453
- attempts.push({
454
- attempt: attemptIndex + 1,
455
- node_id: currentNodeId,
456
- stale_node: stale,
457
- error: error?.message || String(error)
458
- });
459
- if (!stale || attemptIndex >= limit - 1 || !candidateKey) {
460
- error.recommend_detail_open_attempts = attempts;
461
- throw error;
462
- }
456
+ } catch (error) {
457
+ const stale = isStaleRecommendNodeError(error);
458
+ const detailOpenMiss = isRecommendDetailOpenMissError(error);
459
+ attempts.push({
460
+ attempt: attemptIndex + 1,
461
+ node_id: currentNodeId,
462
+ stale_node: stale,
463
+ detail_open_miss: detailOpenMiss,
464
+ error: error?.message || String(error)
465
+ });
466
+ if ((!stale && !detailOpenMiss) || attemptIndex >= limit - 1 || !candidateKey) {
467
+ error.recommend_detail_open_attempts = attempts;
468
+ throw error;
469
+ }
463
470
 
464
471
  const resolved = await findRecommendCardNodeForCandidateKey(client, {
465
472
  candidateKey,
@@ -46,13 +46,14 @@ import {
46
46
  screenCandidate
47
47
  } from "../../core/screening/index.js";
48
48
  import {
49
- closeRecommendDetail,
50
- createRecommendDetailNetworkRecorder,
51
- extractRecommendDetailCandidate,
52
- isStaleRecommendNodeError,
53
- openRecommendCardDetailWithFreshRetry,
54
- waitForRecommendDetailNetworkEvents
55
- } from "./detail.js";
49
+ closeRecommendDetail,
50
+ createRecommendDetailNetworkRecorder,
51
+ extractRecommendDetailCandidate,
52
+ isRecommendDetailOpenMissError,
53
+ isStaleRecommendNodeError,
54
+ openRecommendCardDetailWithFreshRetry,
55
+ waitForRecommendDetailNetworkEvents
56
+ } from "./detail.js";
56
57
  import {
57
58
  readRecommendCardCandidate,
58
59
  waitForRecommendCardNodeIds
@@ -416,12 +417,13 @@ export function countRecommendResultStatuses(results = [], {
416
417
  detail_open_failed: results.filter((item) => (
417
418
  item.error?.code === "DETAIL_STALE_NODE"
418
419
  || item.error?.code === "DETAIL_OPEN_FAILED"
419
- )).length,
420
- transient_recovered: results.filter((item) => (
421
- item.error?.code === "DETAIL_STALE_NODE"
422
- || item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
423
- || item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
424
- || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
420
+ )).length,
421
+ transient_recovered: results.filter((item) => (
422
+ item.error?.code === "DETAIL_STALE_NODE"
423
+ || item.error?.code === "DETAIL_OPEN_FAILED"
424
+ || item.error?.code === "IMAGE_CAPTURE_STALE_NODE"
425
+ || item.error?.code === "IMAGE_CAPTURE_TIMEOUT"
426
+ || item.error?.code === "IMAGE_CAPTURE_TOTAL_TIMEOUT"
425
427
  )).length
426
428
  };
427
429
  }
@@ -461,6 +463,9 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
461
463
  if (error.passed_count != null) {
462
464
  result.passed_count = error.passed_count;
463
465
  }
466
+ if (Array.isArray(error.recommend_detail_open_attempts)) {
467
+ result.recommend_detail_open_attempts = error.recommend_detail_open_attempts;
468
+ }
464
469
  return result;
465
470
  }
466
471
 
@@ -548,9 +553,9 @@ function createImageCaptureFailureScreening(candidate, error) {
548
553
  };
549
554
  }
550
555
 
551
- export function isRecoverableRecommendDetailError(error) {
552
- return isStaleRecommendNodeError(error);
553
- }
556
+ export function isRecoverableRecommendDetailError(error) {
557
+ return isStaleRecommendNodeError(error) || isRecommendDetailOpenMissError(error);
558
+ }
554
559
 
555
560
  function compactRecoverableDetailError(error) {
556
561
  return compactError(error, isStaleRecommendNodeError(error) ? "DETAIL_STALE_NODE" : "DETAIL_OPEN_FAILED");
@@ -560,10 +565,12 @@ function createRecoverableDetailFailureScreening(candidate, error) {
560
565
  return {
561
566
  status: "fail",
562
567
  passed: false,
563
- score: 0,
564
- reasons: isStaleRecommendNodeError(error)
565
- ? ["detail_open_failed", "stale_node"]
566
- : ["detail_open_failed"],
568
+ score: 0,
569
+ reasons: isStaleRecommendNodeError(error)
570
+ ? ["detail_open_failed", "stale_node"]
571
+ : isRecommendDetailOpenMissError(error)
572
+ ? ["detail_open_failed", "detail_open_miss"]
573
+ : ["detail_open_failed"],
567
574
  error: compactRecoverableDetailError(error),
568
575
  candidate
569
576
  };