@reconcrap/boss-recommend-mcp 2.0.15 → 2.0.16
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 +1 -1
- package/src/domains/chat/detail.js +68 -0
- package/src/domains/chat/run-service.js +138 -66
package/package.json
CHANGED
|
@@ -39,13 +39,65 @@ import {
|
|
|
39
39
|
import {
|
|
40
40
|
assertChatShellNotResumeTopLevel,
|
|
41
41
|
getChatTopLevelState,
|
|
42
|
+
isForbiddenChatResumeTopLevelUrl,
|
|
42
43
|
makeForbiddenChatResumeNavigationError
|
|
43
44
|
} from "./page-guard.js";
|
|
44
45
|
|
|
46
|
+
export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
|
|
47
|
+
|
|
45
48
|
export function matchesChatProfileNetwork(url) {
|
|
46
49
|
return CHAT_PROFILE_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
function looksLikeForbiddenChatResumePath(value = "") {
|
|
53
|
+
const normalized = String(value || "");
|
|
54
|
+
return isForbiddenChatResumeTopLevelUrl(normalized)
|
|
55
|
+
|| /(?:^|["'\s=])(?:https?:\/\/[^"'\s>]*zhipin\.com)?\/web\/frame\/c-resume(?:[/?#"' >]|$)/i
|
|
56
|
+
.test(normalized);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractFirstHtmlAttribute(html = "", names = []) {
|
|
60
|
+
const source = String(html || "");
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
const escaped = String(name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
63
|
+
const regex = new RegExp(`${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "i");
|
|
64
|
+
const match = source.match(regex);
|
|
65
|
+
if (match) return match[1] ?? match[2] ?? match[3] ?? "";
|
|
66
|
+
}
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isUnsafeChatOnlineResumeTarget(target = {}, buttonHTML = "") {
|
|
71
|
+
const attributes = target?.attributes || {};
|
|
72
|
+
const href = attributes.href
|
|
73
|
+
|| attributes["data-href"]
|
|
74
|
+
|| attributes["data-url"]
|
|
75
|
+
|| attributes.url
|
|
76
|
+
|| extractFirstHtmlAttribute(buttonHTML, ["href", "data-href", "data-url", "url"]);
|
|
77
|
+
return looksLikeForbiddenChatResumePath(href)
|
|
78
|
+
|| looksLikeForbiddenChatResumePath(buttonHTML);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function makeUnsafeChatOnlineResumeLinkError(target = {}, buttonHTML = "") {
|
|
82
|
+
const href = target?.attributes?.href
|
|
83
|
+
|| target?.attributes?.["data-href"]
|
|
84
|
+
|| target?.attributes?.["data-url"]
|
|
85
|
+
|| extractFirstHtmlAttribute(buttonHTML, ["href", "data-href", "data-url", "url"])
|
|
86
|
+
|| null;
|
|
87
|
+
const error = new Error("CHAT_UNSAFE_ONLINE_RESUME_LINK: refusing to click an online resume link that can navigate the chat tab to /web/frame/c-resume/");
|
|
88
|
+
error.code = CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE;
|
|
89
|
+
error.href = href;
|
|
90
|
+
error.button_selector = target?.selector || null;
|
|
91
|
+
error.button_text = htmlToText(buttonHTML).slice(0, 120);
|
|
92
|
+
error.button_html_length = String(buttonHTML || "").length;
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isUnsafeChatOnlineResumeLinkError(error) {
|
|
97
|
+
return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
|
|
98
|
+
|| /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
|
|
99
|
+
}
|
|
100
|
+
|
|
49
101
|
export function createChatProfileNetworkRecorder(client) {
|
|
50
102
|
const events = [];
|
|
51
103
|
client.Network.responseReceived((event) => {
|
|
@@ -673,6 +725,22 @@ export async function openChatOnlineResume(client, {
|
|
|
673
725
|
buttonHTML = await getOuterHTML(client, buttonState.target.node_id);
|
|
674
726
|
} catch {}
|
|
675
727
|
|
|
728
|
+
if (isUnsafeChatOnlineResumeTarget(buttonState.target, buttonHTML)) {
|
|
729
|
+
const error = makeUnsafeChatOnlineResumeLinkError(buttonState.target, buttonHTML);
|
|
730
|
+
attempts.push({
|
|
731
|
+
attempt: index + 1,
|
|
732
|
+
ok: false,
|
|
733
|
+
error: error.code,
|
|
734
|
+
blocked_pre_click: true,
|
|
735
|
+
button_selector: buttonState.target.selector,
|
|
736
|
+
button_text: error.button_text,
|
|
737
|
+
button_href: error.href,
|
|
738
|
+
button_html_length: buttonHTML.length
|
|
739
|
+
});
|
|
740
|
+
error.attempts = attempts;
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
|
|
676
744
|
try {
|
|
677
745
|
if (buttonState.target.center) {
|
|
678
746
|
await clickPoint(client, buttonState.target.center.x, buttonState.target.center.y);
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
closeChatResumeModal,
|
|
50
50
|
createChatProfileNetworkRecorder,
|
|
51
51
|
extractChatProfileCandidate,
|
|
52
|
+
isUnsafeChatOnlineResumeLinkError,
|
|
52
53
|
openChatOnlineResume,
|
|
53
54
|
readChatConversationReadyState,
|
|
54
55
|
requestChatResumeForPassedCandidate,
|
|
@@ -205,6 +206,8 @@ function createSkippedDetailResult(cardCandidate, reason, error = null) {
|
|
|
205
206
|
|
|
206
207
|
const CHAT_FULL_CV_DOM_MIN_TEXT_LENGTH = 500;
|
|
207
208
|
const CHAT_FULL_CV_DOM_MIN_SECTION_TEXT_LENGTH = 180;
|
|
209
|
+
const CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH = 650;
|
|
210
|
+
const CHAT_FULL_CV_NETWORK_MIN_RICH_ITEM_COUNT = 3;
|
|
208
211
|
const CHAT_FULL_CV_SECTION_PATTERNS = Object.freeze([
|
|
209
212
|
/教育(?:经历|背景|经验)?/i,
|
|
210
213
|
/工作(?:经历|经验)?/i,
|
|
@@ -250,17 +253,32 @@ function isFullCvNetworkProfile(profileResult = {}) {
|
|
|
250
253
|
const sourceKeys = profileResult.profile?.source_keys || {};
|
|
251
254
|
const textLength = networkProfileTextLength(profileResult);
|
|
252
255
|
const sectionCount = resumeSectionMatchCount(profileResult.profile?.text || "");
|
|
256
|
+
const richItemCount = [
|
|
257
|
+
"education_count",
|
|
258
|
+
"work_count",
|
|
259
|
+
"project_count",
|
|
260
|
+
"expectation_count",
|
|
261
|
+
"certification_count"
|
|
262
|
+
].reduce((sum, key) => sum + (Number(sourceKeys[key]) || 0), 0);
|
|
263
|
+
const hasResumeSections = sectionCount >= 3 || (sectionCount >= 2 && richItemCount >= 2);
|
|
264
|
+
const hasEnoughNetworkText = textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH;
|
|
253
265
|
|
|
254
|
-
if (sourceKeys.geek_detail_info || sourceKeys.geek_detail)
|
|
266
|
+
if (sourceKeys.geek_detail_info || sourceKeys.geek_detail) {
|
|
267
|
+
return hasEnoughNetworkText && (
|
|
268
|
+
hasResumeSections
|
|
269
|
+
|| richItemCount >= CHAT_FULL_CV_NETWORK_MIN_RICH_ITEM_COUNT
|
|
270
|
+
);
|
|
271
|
+
}
|
|
255
272
|
if (sourceKeys.network_html_text) {
|
|
256
|
-
return textLength >=
|
|
257
|
-
|
|
273
|
+
return textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH
|
|
274
|
+
&& sectionCount >= 2;
|
|
258
275
|
}
|
|
259
276
|
if (sourceKeys.chat_history_resume) {
|
|
260
277
|
const educationCount = Number(sourceKeys.education_count) || 0;
|
|
261
278
|
const workCount = Number(sourceKeys.work_count) || 0;
|
|
262
|
-
return (educationCount + workCount)
|
|
263
|
-
&&
|
|
279
|
+
return (educationCount + workCount) >= 2
|
|
280
|
+
&& textLength >= CHAT_FULL_CV_NETWORK_MIN_TEXT_LENGTH
|
|
281
|
+
&& sectionCount >= 2;
|
|
264
282
|
}
|
|
265
283
|
return false;
|
|
266
284
|
}
|
|
@@ -826,28 +844,8 @@ export async function runChatWorkflow({
|
|
|
826
844
|
}
|
|
827
845
|
|
|
828
846
|
if (!detailResult) {
|
|
829
|
-
detailStep = "open_online_resume";
|
|
830
|
-
networkRecorder.clear();
|
|
831
|
-
const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
|
|
832
|
-
timeoutMs: readyTimeoutMs
|
|
833
|
-
}));
|
|
834
847
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
835
|
-
|
|
836
|
-
const networkWait = ["network", "cascade"].includes(normalizedDetailSource)
|
|
837
|
-
? await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
838
|
-
waitForChatProfileNetworkEvents,
|
|
839
|
-
networkRecorder,
|
|
840
|
-
{
|
|
841
|
-
waitPlan,
|
|
842
|
-
minCount: 1,
|
|
843
|
-
requireLoaded: true,
|
|
844
|
-
intervalMs: 200
|
|
845
|
-
}
|
|
846
|
-
))
|
|
847
|
-
: null;
|
|
848
|
-
if (networkWait?.elapsed_ms != null) {
|
|
849
|
-
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
850
|
-
}
|
|
848
|
+
let networkWait = null;
|
|
851
849
|
let contentWait = {
|
|
852
850
|
ok: false,
|
|
853
851
|
skipped: false,
|
|
@@ -855,39 +853,44 @@ export async function runChatWorkflow({
|
|
|
855
853
|
elapsed_ms: 0,
|
|
856
854
|
text_length: 0
|
|
857
855
|
};
|
|
858
|
-
let resumeState =
|
|
856
|
+
let resumeState = null;
|
|
859
857
|
let resumeHtml = null;
|
|
860
|
-
let resumeNetworkEvents =
|
|
858
|
+
let resumeNetworkEvents = [];
|
|
861
859
|
let parsedNetworkProfileCount = 0;
|
|
862
860
|
|
|
863
861
|
if (
|
|
864
862
|
["network", "cascade"].includes(normalizedDetailSource)
|
|
865
|
-
&&
|
|
863
|
+
&& selectionNetworkEvents.length > 0
|
|
866
864
|
) {
|
|
867
|
-
detailStep = "
|
|
865
|
+
detailStep = "extract_selection_network_profile";
|
|
868
866
|
detailResult = await extractChatProfileCandidate(client, {
|
|
869
867
|
cardCandidate,
|
|
870
868
|
cardNodeId: effectiveCardNodeId,
|
|
871
|
-
resumeState,
|
|
872
|
-
resumeHtml,
|
|
873
|
-
networkEvents:
|
|
874
|
-
normalizedDetailSource,
|
|
875
|
-
selectionNetworkEvents,
|
|
876
|
-
resumeNetworkEvents
|
|
877
|
-
),
|
|
869
|
+
resumeState: null,
|
|
870
|
+
resumeHtml: null,
|
|
871
|
+
networkEvents: selectionNetworkEvents,
|
|
878
872
|
targetUrl,
|
|
879
873
|
closeResume: false,
|
|
880
|
-
networkParseRetryMs: waitPlan.mode_before === "image" ?
|
|
881
|
-
networkParseIntervalMs:
|
|
874
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 250 : 900,
|
|
875
|
+
networkParseIntervalMs: 150
|
|
882
876
|
});
|
|
883
877
|
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
884
878
|
parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
885
|
-
const
|
|
886
|
-
if (
|
|
879
|
+
const selectionNetworkEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
|
|
880
|
+
if (selectionNetworkEvidence.network_full_cv_count > 0) {
|
|
881
|
+
networkWait = {
|
|
882
|
+
ok: true,
|
|
883
|
+
skipped: true,
|
|
884
|
+
reason: "selection_network_full_cv_before_online_resume_click",
|
|
885
|
+
elapsed_ms: detailResult.network_parse_retry_elapsed_ms,
|
|
886
|
+
count: selectionNetworkEvents.length,
|
|
887
|
+
total_event_count: selectionNetworkEvents.length,
|
|
888
|
+
wait_plan: waitPlan
|
|
889
|
+
};
|
|
887
890
|
contentWait = {
|
|
888
891
|
ok: true,
|
|
889
892
|
skipped: true,
|
|
890
|
-
reason: "
|
|
893
|
+
reason: "selection_network_full_cv_before_online_resume_click",
|
|
891
894
|
elapsed_ms: 0,
|
|
892
895
|
text_length: 0
|
|
893
896
|
};
|
|
@@ -897,32 +900,94 @@ export async function runChatWorkflow({
|
|
|
897
900
|
}
|
|
898
901
|
|
|
899
902
|
if (!detailResult) {
|
|
900
|
-
detailStep = "
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
903
|
+
detailStep = "open_online_resume";
|
|
904
|
+
networkRecorder.clear();
|
|
905
|
+
const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
|
|
906
|
+
timeoutMs: readyTimeoutMs
|
|
904
907
|
}));
|
|
905
|
-
resumeState =
|
|
906
|
-
|
|
908
|
+
resumeState = openedResume.resume_state;
|
|
909
|
+
detailStep = "wait_network";
|
|
910
|
+
networkWait = ["network", "cascade"].includes(normalizedDetailSource)
|
|
911
|
+
? await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
912
|
+
waitForChatProfileNetworkEvents,
|
|
913
|
+
networkRecorder,
|
|
914
|
+
{
|
|
915
|
+
waitPlan,
|
|
916
|
+
minCount: 1,
|
|
917
|
+
requireLoaded: true,
|
|
918
|
+
intervalMs: 200
|
|
919
|
+
}
|
|
920
|
+
))
|
|
921
|
+
: null;
|
|
922
|
+
if (networkWait?.elapsed_ms != null) {
|
|
923
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
924
|
+
}
|
|
907
925
|
resumeNetworkEvents = networkRecorder.events.slice();
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
+
|
|
927
|
+
if (
|
|
928
|
+
["network", "cascade"].includes(normalizedDetailSource)
|
|
929
|
+
&& networkWait?.count > 0
|
|
930
|
+
) {
|
|
931
|
+
detailStep = "extract_network_profile";
|
|
932
|
+
detailResult = await extractChatProfileCandidate(client, {
|
|
933
|
+
cardCandidate,
|
|
934
|
+
cardNodeId: effectiveCardNodeId,
|
|
935
|
+
resumeState,
|
|
936
|
+
resumeHtml,
|
|
937
|
+
networkEvents: selectedDetailNetworkEvents(
|
|
938
|
+
normalizedDetailSource,
|
|
939
|
+
selectionNetworkEvents,
|
|
940
|
+
resumeNetworkEvents
|
|
941
|
+
),
|
|
942
|
+
targetUrl,
|
|
943
|
+
closeResume: false,
|
|
944
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
945
|
+
networkParseIntervalMs: 250
|
|
946
|
+
});
|
|
947
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
948
|
+
parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
949
|
+
const networkEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
|
|
950
|
+
if (networkEvidence.network_full_cv_count > 0) {
|
|
951
|
+
contentWait = {
|
|
952
|
+
ok: true,
|
|
953
|
+
skipped: true,
|
|
954
|
+
reason: "network_full_cv_parsed_before_dom_wait",
|
|
955
|
+
elapsed_ms: 0,
|
|
956
|
+
text_length: 0
|
|
957
|
+
};
|
|
958
|
+
} else {
|
|
959
|
+
detailResult = null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (!detailResult) {
|
|
964
|
+
detailStep = "wait_resume_content";
|
|
965
|
+
contentWait = await measureTiming(timings, "dom_fallback_ms", () => waitForChatResumeContent(client, {
|
|
966
|
+
timeoutMs: resumeDomTimeoutMs,
|
|
967
|
+
intervalMs: 300
|
|
968
|
+
}));
|
|
969
|
+
resumeState = contentWait.resume_state || openedResume.resume_state;
|
|
970
|
+
resumeHtml = contentWait.resume_html || null;
|
|
971
|
+
resumeNetworkEvents = networkRecorder.events.slice();
|
|
972
|
+
detailStep = "extract_resume_content";
|
|
973
|
+
detailResult = await extractChatProfileCandidate(client, {
|
|
974
|
+
cardCandidate,
|
|
975
|
+
cardNodeId: effectiveCardNodeId,
|
|
976
|
+
resumeState,
|
|
977
|
+
resumeHtml,
|
|
978
|
+
networkEvents: selectedDetailNetworkEvents(
|
|
979
|
+
normalizedDetailSource,
|
|
980
|
+
selectionNetworkEvents,
|
|
981
|
+
resumeNetworkEvents
|
|
982
|
+
),
|
|
983
|
+
targetUrl,
|
|
984
|
+
closeResume: false,
|
|
985
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
986
|
+
networkParseIntervalMs: 250
|
|
987
|
+
});
|
|
988
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
989
|
+
parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
990
|
+
}
|
|
926
991
|
}
|
|
927
992
|
|
|
928
993
|
let source = normalizedDetailSource === "dom" ? "dom" : "network";
|
|
@@ -1113,6 +1178,13 @@ export async function runChatWorkflow({
|
|
|
1113
1178
|
const recovery = await recoverAndReapplyChatContext(detailUnavailableReason, error);
|
|
1114
1179
|
detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
|
|
1115
1180
|
detailResult.cv_acquisition.recovery = recovery;
|
|
1181
|
+
} else if (isUnsafeChatOnlineResumeLinkError(error)) {
|
|
1182
|
+
detailUnavailableReason = "unsafe_online_resume_navigation_link";
|
|
1183
|
+
detailResult = createSkippedDetailResult(cardCandidate, detailUnavailableReason, error);
|
|
1184
|
+
detailResult.cv_acquisition.blocked_pre_click = true;
|
|
1185
|
+
detailResult.cv_acquisition.button_href = error.href || null;
|
|
1186
|
+
detailResult.cv_acquisition.button_selector = error.button_selector || null;
|
|
1187
|
+
detailResult.cv_acquisition.attempts = error.attempts || null;
|
|
1116
1188
|
} else {
|
|
1117
1189
|
if (!isRecoverableCdpNodeError(error)) throw error;
|
|
1118
1190
|
detailUnavailableReason = `recoverable_cdp_node_stale:${detailStep}`;
|