@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
|
@@ -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
|
|
876
|
-
return
|
|
877
|
-
|
|
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
|
-
|
|
884
|
-
|
|
959
|
+
return invokeWithReconnect({
|
|
960
|
+
methodNameForLog: method,
|
|
961
|
+
invoke: (activeClient) => activeClient.send(method, params)
|
|
962
|
+
});
|
|
885
963
|
};
|
|
886
964
|
}
|
|
887
965
|
|
|
888
|
-
|
|
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(
|
|
892
|
-
get(
|
|
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
|
|
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
|
-
|
|
902
|
-
|
|
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
|
-
|
|
1041
|
+
let rawClient = await CDP({ host, port, target });
|
|
1042
|
+
let activeTarget = target;
|
|
932
1043
|
const methodLog = [];
|
|
933
|
-
const client = createGuardedCdpClient(rawClient, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
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 =
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 === "
|
|
423
|
-
|| item.error?.code === "
|
|
424
|
-
|| item.error?.code === "
|
|
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
|
-
:
|
|
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
|
};
|