@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.
- package/package.json +1 -1
- package/src/chat-mcp.js +12 -3
- package/src/core/boss-cards/index.js +199 -0
- package/src/core/capture/index.js +126 -24
- package/src/core/cv-acquisition/index.js +6 -0
- package/src/core/reporting/legacy-csv.js +10 -1
- package/src/core/run/timing.js +33 -0
- package/src/core/screening/index.js +310 -27
- package/src/domains/chat/cards.js +9 -1
- package/src/domains/chat/detail.js +29 -18
- package/src/domains/chat/run-service.js +72 -28
- package/src/domains/recommend/cards.js +16 -1
- package/src/domains/recommend/detail.js +40 -19
- package/src/domains/recommend/run-service.js +60 -15
- package/src/domains/recruit/cards.js +9 -1
- package/src/domains/recruit/detail.js +41 -19
- package/src/domains/recruit/run-service.js +58 -15
- package/src/index.js +2 -2
- package/src/recommend-mcp.js +12 -3
- package/src/recruit-mcp.js +13 -4
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
} from "../../core/infinite-list/index.js";
|
|
25
25
|
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
26
26
|
import { createRunLifecycleManager } from "../../core/run/index.js";
|
|
27
|
+
import {
|
|
28
|
+
addTiming,
|
|
29
|
+
imageEvidenceFilePath,
|
|
30
|
+
measureTiming
|
|
31
|
+
} from "../../core/run/timing.js";
|
|
27
32
|
import {
|
|
28
33
|
callScreeningLlm,
|
|
29
34
|
normalizeText,
|
|
@@ -369,7 +374,8 @@ export async function runChatWorkflow({
|
|
|
369
374
|
listStableSignatureLimit = 2,
|
|
370
375
|
listWheelDeltaY = 850,
|
|
371
376
|
listSettleMs = 1200,
|
|
372
|
-
listFallbackPoint = null
|
|
377
|
+
listFallbackPoint = null,
|
|
378
|
+
imageOutputDir = ""
|
|
373
379
|
} = {}, runControl) {
|
|
374
380
|
if (!client) throw new Error("runChatWorkflow requires a guarded CDP client");
|
|
375
381
|
const normalizedDetailSource = normalizeDetailSource(detailSource);
|
|
@@ -584,6 +590,8 @@ export async function runChatWorkflow({
|
|
|
584
590
|
|| results.filter((item) => item.screening?.passed).length < passTarget
|
|
585
591
|
)
|
|
586
592
|
) {
|
|
593
|
+
const candidateStarted = Date.now();
|
|
594
|
+
const timings = {};
|
|
587
595
|
await runControl.waitIfPaused();
|
|
588
596
|
runControl.throwIfCanceled();
|
|
589
597
|
runControl.setPhase("chat:candidate");
|
|
@@ -596,7 +604,7 @@ export async function runChatWorkflow({
|
|
|
596
604
|
continue;
|
|
597
605
|
}
|
|
598
606
|
|
|
599
|
-
const nextCandidateResult = await getNextInfiniteListCandidate({
|
|
607
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
600
608
|
client,
|
|
601
609
|
state: listState,
|
|
602
610
|
maxScrolls: listMaxScrolls,
|
|
@@ -623,7 +631,7 @@ export async function runChatWorkflow({
|
|
|
623
631
|
visible_index: visibleIndex
|
|
624
632
|
}
|
|
625
633
|
})
|
|
626
|
-
});
|
|
634
|
+
}));
|
|
627
635
|
if (!nextCandidateResult.ok) {
|
|
628
636
|
const endTopLevelState = await getChatTopLevelState(client);
|
|
629
637
|
if (!endTopLevelState.is_chat_shell) {
|
|
@@ -665,11 +673,11 @@ export async function runChatWorkflow({
|
|
|
665
673
|
|
|
666
674
|
detailStep = "select_candidate";
|
|
667
675
|
networkRecorder.clear();
|
|
668
|
-
const selected = await selectFreshChatCandidate(client, {
|
|
676
|
+
const selected = await measureTiming(timings, "candidate_click_ms", () => selectFreshChatCandidate(client, {
|
|
669
677
|
cardNodeId,
|
|
670
678
|
candidate: cardCandidate,
|
|
671
679
|
timeoutMs: onlineResumeButtonTimeoutMs
|
|
672
|
-
});
|
|
680
|
+
}));
|
|
673
681
|
if (selected.ready?.forbidden_top_level_navigation) {
|
|
674
682
|
throw makeForbiddenChatResumeNavigationError(selected.ready.top_level_state);
|
|
675
683
|
}
|
|
@@ -696,13 +704,13 @@ export async function runChatWorkflow({
|
|
|
696
704
|
if (!detailResult) {
|
|
697
705
|
detailStep = "open_online_resume";
|
|
698
706
|
networkRecorder.clear();
|
|
699
|
-
const openedResume = await openChatOnlineResume(client, {
|
|
707
|
+
const openedResume = await measureTiming(timings, "detail_open_ms", () => openChatOnlineResume(client, {
|
|
700
708
|
timeoutMs: readyTimeoutMs
|
|
701
|
-
});
|
|
709
|
+
}));
|
|
702
710
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
703
711
|
detailStep = "wait_network";
|
|
704
712
|
const networkWait = ["network", "cascade"].includes(normalizedDetailSource)
|
|
705
|
-
? await waitForCvNetworkEvents(
|
|
713
|
+
? await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
706
714
|
waitForChatProfileNetworkEvents,
|
|
707
715
|
networkRecorder,
|
|
708
716
|
{
|
|
@@ -711,8 +719,11 @@ export async function runChatWorkflow({
|
|
|
711
719
|
requireLoaded: true,
|
|
712
720
|
intervalMs: 200
|
|
713
721
|
}
|
|
714
|
-
)
|
|
722
|
+
))
|
|
715
723
|
: null;
|
|
724
|
+
if (networkWait?.elapsed_ms != null) {
|
|
725
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
726
|
+
}
|
|
716
727
|
let contentWait = {
|
|
717
728
|
ok: false,
|
|
718
729
|
skipped: false,
|
|
@@ -741,8 +752,11 @@ export async function runChatWorkflow({
|
|
|
741
752
|
resumeNetworkEvents
|
|
742
753
|
),
|
|
743
754
|
targetUrl,
|
|
744
|
-
closeResume: false
|
|
755
|
+
closeResume: false,
|
|
756
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
757
|
+
networkParseIntervalMs: 250
|
|
745
758
|
});
|
|
759
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
746
760
|
parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
747
761
|
if (parsedNetworkProfileCount > 0) {
|
|
748
762
|
contentWait = {
|
|
@@ -759,10 +773,10 @@ export async function runChatWorkflow({
|
|
|
759
773
|
|
|
760
774
|
if (!detailResult) {
|
|
761
775
|
detailStep = "wait_resume_content";
|
|
762
|
-
contentWait = await waitForChatResumeContent(client, {
|
|
776
|
+
contentWait = await measureTiming(timings, "dom_fallback_ms", () => waitForChatResumeContent(client, {
|
|
763
777
|
timeoutMs: resumeDomTimeoutMs,
|
|
764
778
|
intervalMs: 300
|
|
765
|
-
});
|
|
779
|
+
}));
|
|
766
780
|
resumeState = contentWait.resume_state || openedResume.resume_state;
|
|
767
781
|
resumeHtml = contentWait.resume_html || null;
|
|
768
782
|
resumeNetworkEvents = networkRecorder.events.slice();
|
|
@@ -778,8 +792,11 @@ export async function runChatWorkflow({
|
|
|
778
792
|
resumeNetworkEvents
|
|
779
793
|
),
|
|
780
794
|
targetUrl,
|
|
781
|
-
closeResume: false
|
|
795
|
+
closeResume: false,
|
|
796
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
797
|
+
networkParseIntervalMs: 250
|
|
782
798
|
});
|
|
799
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
783
800
|
parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
784
801
|
}
|
|
785
802
|
|
|
@@ -792,11 +809,25 @@ export async function runChatWorkflow({
|
|
|
792
809
|
if (shouldCaptureImage) {
|
|
793
810
|
if (captureNodeId) {
|
|
794
811
|
detailStep = "capture_image_fallback";
|
|
795
|
-
imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
812
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
813
|
+
filePath: imageEvidenceFilePath({
|
|
814
|
+
imageOutputDir,
|
|
815
|
+
domain: "chat",
|
|
816
|
+
runId: runControl?.runId,
|
|
817
|
+
index,
|
|
818
|
+
extension: "jpg"
|
|
819
|
+
}),
|
|
820
|
+
format: "jpeg",
|
|
821
|
+
quality: 72,
|
|
822
|
+
optimize: true,
|
|
823
|
+
resizeMaxWidth: 1100,
|
|
824
|
+
captureViewport: true,
|
|
796
825
|
padding: 8,
|
|
797
826
|
maxScreenshots: maxImagePages,
|
|
798
827
|
wheelDeltaY: imageWheelDeltaY,
|
|
799
|
-
settleMs:
|
|
828
|
+
settleMs: 350,
|
|
829
|
+
duplicateStopCount: 1,
|
|
830
|
+
skipDuplicateScreenshots: true,
|
|
800
831
|
metadata: {
|
|
801
832
|
domain: "chat",
|
|
802
833
|
capture_mode: "scroll_sequence",
|
|
@@ -806,7 +837,7 @@ export async function runChatWorkflow({
|
|
|
806
837
|
run_candidate_index: index,
|
|
807
838
|
candidate_key: candidateKey
|
|
808
839
|
}
|
|
809
|
-
});
|
|
840
|
+
}));
|
|
810
841
|
source = "image";
|
|
811
842
|
recordCvImageFallback(cvAcquisitionState, {
|
|
812
843
|
parsedNetworkProfileCount,
|
|
@@ -819,7 +850,7 @@ export async function runChatWorkflow({
|
|
|
819
850
|
llmResult = createMissingLlmConfigResult();
|
|
820
851
|
} else {
|
|
821
852
|
try {
|
|
822
|
-
llmResult = await callScreeningLlm({
|
|
853
|
+
llmResult = await measureTiming(timings, "vision_model_ms", () => callScreeningLlm({
|
|
823
854
|
candidate: detailResult.candidate,
|
|
824
855
|
criteria,
|
|
825
856
|
config: llmConfig,
|
|
@@ -827,7 +858,7 @@ export async function runChatWorkflow({
|
|
|
827
858
|
imageEvidence,
|
|
828
859
|
maxImages: llmImageLimit,
|
|
829
860
|
imageDetail: llmImageDetail
|
|
830
|
-
});
|
|
861
|
+
}));
|
|
831
862
|
} catch (error) {
|
|
832
863
|
llmResult = createFailedLlmResult(error);
|
|
833
864
|
}
|
|
@@ -861,7 +892,10 @@ export async function runChatWorkflow({
|
|
|
861
892
|
llmResult = createMissingLlmConfigResult();
|
|
862
893
|
} else {
|
|
863
894
|
try {
|
|
864
|
-
|
|
895
|
+
const llmTimingKey = imageEvidence?.file_paths?.length
|
|
896
|
+
? "vision_model_ms"
|
|
897
|
+
: "text_model_ms";
|
|
898
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
865
899
|
candidate: detailResult.candidate,
|
|
866
900
|
criteria,
|
|
867
901
|
config: llmConfig,
|
|
@@ -869,7 +903,7 @@ export async function runChatWorkflow({
|
|
|
869
903
|
imageEvidence,
|
|
870
904
|
maxImages: llmImageLimit,
|
|
871
905
|
imageDetail: llmImageDetail
|
|
872
|
-
});
|
|
906
|
+
}));
|
|
873
907
|
} catch (error) {
|
|
874
908
|
llmResult = createFailedLlmResult(error);
|
|
875
909
|
}
|
|
@@ -879,7 +913,7 @@ export async function runChatWorkflow({
|
|
|
879
913
|
let closeResult = null;
|
|
880
914
|
if (closeResume) {
|
|
881
915
|
detailStep = "close_resume_modal";
|
|
882
|
-
closeResult = await closeChatResumeModal(client);
|
|
916
|
+
closeResult = await measureTiming(timings, "close_detail_ms", () => closeChatResumeModal(client));
|
|
883
917
|
}
|
|
884
918
|
detailResult.close_result = closeResult;
|
|
885
919
|
detailResult.image_evidence = imageEvidence;
|
|
@@ -927,14 +961,14 @@ export async function runChatWorkflow({
|
|
|
927
961
|
cardOnlyLlmResult = createMissingLlmConfigResult();
|
|
928
962
|
} else {
|
|
929
963
|
try {
|
|
930
|
-
cardOnlyLlmResult = await callScreeningLlm({
|
|
964
|
+
cardOnlyLlmResult = await measureTiming(timings, "text_model_ms", () => callScreeningLlm({
|
|
931
965
|
candidate: screeningCandidate,
|
|
932
966
|
criteria,
|
|
933
967
|
config: llmConfig,
|
|
934
968
|
timeoutMs: llmTimeoutMs,
|
|
935
969
|
maxImages: llmImageLimit,
|
|
936
970
|
imageDetail: llmImageDetail
|
|
937
|
-
});
|
|
971
|
+
}));
|
|
938
972
|
} catch (error) {
|
|
939
973
|
cardOnlyLlmResult = createFailedLlmResult(error);
|
|
940
974
|
}
|
|
@@ -954,10 +988,10 @@ export async function runChatWorkflow({
|
|
|
954
988
|
: screenCandidate(screeningCandidate, { criteria });
|
|
955
989
|
let postAction = null;
|
|
956
990
|
if (requestResumeForPassed && screening.passed) {
|
|
957
|
-
postAction = await requestChatResumeForPassedCandidate(client, {
|
|
991
|
+
postAction = await measureTiming(timings, "post_action_ms", () => requestChatResumeForPassedCandidate(client, {
|
|
958
992
|
greetingText,
|
|
959
993
|
dryRun: dryRunRequestCv
|
|
960
|
-
});
|
|
994
|
+
}));
|
|
961
995
|
if (postAction?.requested) requestSatisfiedCount += 1;
|
|
962
996
|
if (postAction?.skipped) requestSkippedCount += 1;
|
|
963
997
|
if (postAction?.requested && !postAction?.skipped) requestedCount += 1;
|
|
@@ -965,6 +999,7 @@ export async function runChatWorkflow({
|
|
|
965
999
|
throw new Error(`REQUEST_CV_NOT_VERIFIED:${postAction?.reason || "unknown"}`);
|
|
966
1000
|
}
|
|
967
1001
|
}
|
|
1002
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
968
1003
|
const compactResult = {
|
|
969
1004
|
index,
|
|
970
1005
|
candidate_key: candidateKey,
|
|
@@ -974,7 +1009,8 @@ export async function runChatWorkflow({
|
|
|
974
1009
|
llm_screening: detailResult ? null : compactLlmResult(cardOnlyLlmResult),
|
|
975
1010
|
screening: compactScreening(screening),
|
|
976
1011
|
post_action: postAction,
|
|
977
|
-
pre_action_state: preActionState
|
|
1012
|
+
pre_action_state: preActionState,
|
|
1013
|
+
timings
|
|
978
1014
|
};
|
|
979
1015
|
results.push(compactResult);
|
|
980
1016
|
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
@@ -1006,6 +1042,7 @@ export async function runChatWorkflow({
|
|
|
1006
1042
|
last_candidate_key: candidateKey,
|
|
1007
1043
|
last_score: screening.score
|
|
1008
1044
|
});
|
|
1045
|
+
const checkpointStarted = Date.now();
|
|
1009
1046
|
runControl.checkpoint({
|
|
1010
1047
|
results,
|
|
1011
1048
|
last_candidate: {
|
|
@@ -1020,9 +1057,13 @@ export async function runChatWorkflow({
|
|
|
1020
1057
|
llm_screening: compactLlmResult(effectiveLlmResult)
|
|
1021
1058
|
}
|
|
1022
1059
|
});
|
|
1060
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
1023
1061
|
|
|
1024
1062
|
if (delayMs > 0) {
|
|
1063
|
+
const sleepStarted = Date.now();
|
|
1025
1064
|
await runControl.sleep(delayMs);
|
|
1065
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
1066
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
1026
1067
|
}
|
|
1027
1068
|
}
|
|
1028
1069
|
|
|
@@ -1095,6 +1136,7 @@ export function createChatRunService({
|
|
|
1095
1136
|
listWheelDeltaY = 850,
|
|
1096
1137
|
listSettleMs = 1200,
|
|
1097
1138
|
listFallbackPoint = null,
|
|
1139
|
+
imageOutputDir = "",
|
|
1098
1140
|
name = "chat-domain-run"
|
|
1099
1141
|
} = {}) {
|
|
1100
1142
|
if (!client) throw new Error("startChatRun requires a guarded CDP client");
|
|
@@ -1130,7 +1172,8 @@ export function createChatRunService({
|
|
|
1130
1172
|
list_wheel_delta_y: listWheelDeltaY,
|
|
1131
1173
|
list_settle_ms: listSettleMs,
|
|
1132
1174
|
list_fallback_point: listFallbackPoint,
|
|
1133
|
-
online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs
|
|
1175
|
+
online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
|
|
1176
|
+
image_output_dir: imageOutputDir || ""
|
|
1134
1177
|
},
|
|
1135
1178
|
progress: {
|
|
1136
1179
|
card_count: 0,
|
|
@@ -1180,7 +1223,8 @@ export function createChatRunService({
|
|
|
1180
1223
|
listStableSignatureLimit,
|
|
1181
1224
|
listWheelDeltaY,
|
|
1182
1225
|
listSettleMs,
|
|
1183
|
-
listFallbackPoint
|
|
1226
|
+
listFallbackPoint,
|
|
1227
|
+
imageOutputDir
|
|
1184
1228
|
}, runControl)
|
|
1185
1229
|
});
|
|
1186
1230
|
}
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
querySelectorAll,
|
|
7
7
|
sleep
|
|
8
8
|
} from "../../core/browser/index.js";
|
|
9
|
+
import {
|
|
10
|
+
mergeBossCandidateCardFields,
|
|
11
|
+
parseBossCandidateCardFieldsFromHtml
|
|
12
|
+
} from "../../core/boss-cards/index.js";
|
|
9
13
|
import {
|
|
10
14
|
htmlToText,
|
|
11
15
|
normalizeCandidateFromHtml,
|
|
@@ -24,6 +28,16 @@ function normalizeRefreshButtonLabel(outerHTML = "") {
|
|
|
24
28
|
return normalizeText(htmlToText(outerHTML)).replace(/\s+/g, "");
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
export function parseRecommendCardFieldsFromHtml(html = "") {
|
|
32
|
+
return parseBossCandidateCardFieldsFromHtml(html);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function enrichRecommendCardCandidate(candidate, outerHTML = "") {
|
|
36
|
+
return mergeBossCandidateCardFields(candidate, outerHTML, {
|
|
37
|
+
metadataKey: "recommend_card_fields"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
function isRefreshButtonLabel(label = "") {
|
|
28
42
|
const normalized = String(label || "").trim();
|
|
29
43
|
if (!normalized || normalized.length > 80) return false;
|
|
@@ -91,7 +105,7 @@ export async function readRecommendCardCandidate(client, cardNodeId, {
|
|
|
91
105
|
getAttributesMap(client, cardNodeId),
|
|
92
106
|
getOuterHTML(client, cardNodeId)
|
|
93
107
|
]);
|
|
94
|
-
|
|
108
|
+
const candidate = normalizeCandidateFromHtml({
|
|
95
109
|
domain: "recommend",
|
|
96
110
|
source,
|
|
97
111
|
html: outerHTML,
|
|
@@ -102,6 +116,7 @@ export async function readRecommendCardCandidate(client, cardNodeId, {
|
|
|
102
116
|
...metadata
|
|
103
117
|
}
|
|
104
118
|
});
|
|
119
|
+
return enrichRecommendCardCandidate(candidate, outerHTML);
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
export async function readFirstRecommendCardCandidate(client, frameNodeId, options = {}) {
|
|
@@ -279,15 +279,25 @@ export async function openRecommendCardDetail(client, cardNodeId, {
|
|
|
279
279
|
timeoutMs = 12000,
|
|
280
280
|
scrollIntoView = true
|
|
281
281
|
} = {}) {
|
|
282
|
+
const started = Date.now();
|
|
283
|
+
const clickStarted = Date.now();
|
|
282
284
|
const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
|
|
285
|
+
const candidateClickMs = Date.now() - clickStarted;
|
|
286
|
+
const detailStarted = Date.now();
|
|
283
287
|
const detailState = await waitForRecommendDetail(client, { timeoutMs });
|
|
288
|
+
const detailOpenMs = Date.now() - detailStarted;
|
|
284
289
|
if (!detailState?.popup && !detailState?.resumeIframe) {
|
|
285
290
|
throw new Error("Candidate detail did not open or no known detail selectors mounted");
|
|
286
291
|
}
|
|
287
292
|
|
|
288
293
|
return {
|
|
289
294
|
card_box: cardBox,
|
|
290
|
-
detail_state: detailState
|
|
295
|
+
detail_state: detailState,
|
|
296
|
+
timings: {
|
|
297
|
+
candidate_click_ms: candidateClickMs,
|
|
298
|
+
detail_open_ms: detailOpenMs,
|
|
299
|
+
open_total_ms: Date.now() - started
|
|
300
|
+
}
|
|
291
301
|
};
|
|
292
302
|
}
|
|
293
303
|
|
|
@@ -477,31 +487,40 @@ export async function extractRecommendDetailCandidate(client, {
|
|
|
477
487
|
detailState,
|
|
478
488
|
networkEvents = [],
|
|
479
489
|
targetUrl = "",
|
|
480
|
-
closeDetail = true
|
|
490
|
+
closeDetail = true,
|
|
491
|
+
networkParseRetryMs = 1800,
|
|
492
|
+
networkParseIntervalMs = 250
|
|
481
493
|
} = {}) {
|
|
482
|
-
await sleep(1000);
|
|
483
|
-
const networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
|
|
484
494
|
const detailHtml = await readRecommendDetailHtml(client, detailState);
|
|
485
495
|
const detailText = [
|
|
486
496
|
detailHtml.popupText,
|
|
487
497
|
detailHtml.resumeText
|
|
488
498
|
].filter(Boolean).join("\n\n");
|
|
489
499
|
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
500
|
+
const parseStarted = Date.now();
|
|
501
|
+
let networkBodies = [];
|
|
502
|
+
let detailCandidateResult = null;
|
|
503
|
+
do {
|
|
504
|
+
networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
|
|
505
|
+
detailCandidateResult = buildScreeningCandidateFromDetail({
|
|
506
|
+
cardCandidate,
|
|
507
|
+
detailText,
|
|
508
|
+
networkBodies,
|
|
509
|
+
metadata: {
|
|
510
|
+
target_url: targetUrl,
|
|
511
|
+
card_node_id: cardNodeId,
|
|
512
|
+
detail_popup_selector: detailState?.popup?.selector || null,
|
|
513
|
+
detail_popup_root: detailState?.popup?.root || null,
|
|
514
|
+
resume_iframe_selector: detailState?.resumeIframe?.selector || null,
|
|
515
|
+
resume_iframe_root: detailState?.resumeIframe?.root || null,
|
|
516
|
+
resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
|
|
517
|
+
detail_html_errors: detailHtml.errors || []
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
|
|
521
|
+
if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
|
|
522
|
+
await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
|
|
523
|
+
} while (true);
|
|
505
524
|
|
|
506
525
|
let closeResult = null;
|
|
507
526
|
if (closeDetail) {
|
|
@@ -512,6 +531,8 @@ export async function extractRecommendDetailCandidate(client, {
|
|
|
512
531
|
candidate: detailCandidateResult.candidate,
|
|
513
532
|
parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
|
|
514
533
|
network_bodies: networkBodies,
|
|
534
|
+
network_parse_retry_elapsed_ms: Date.now() - parseStarted,
|
|
535
|
+
network_event_count: networkEvents.length,
|
|
515
536
|
detail: {
|
|
516
537
|
popup_text: detailHtml.popupText,
|
|
517
538
|
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 { sleep } from "../../core/browser/index.js";
|
|
4
9
|
import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
|
|
@@ -377,7 +382,8 @@ export async function runRecommendWorkflow({
|
|
|
377
382
|
llmConfig = null,
|
|
378
383
|
llmTimeoutMs = 120000,
|
|
379
384
|
llmImageLimit = 8,
|
|
380
|
-
llmImageDetail = "high"
|
|
385
|
+
llmImageDetail = "high",
|
|
386
|
+
imageOutputDir = ""
|
|
381
387
|
} = {}, runControl) {
|
|
382
388
|
if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
|
|
383
389
|
const normalizedFilter = normalizeFilter(filter);
|
|
@@ -520,12 +526,14 @@ export async function runRecommendWorkflow({
|
|
|
520
526
|
});
|
|
521
527
|
|
|
522
528
|
while (results.length < limit) {
|
|
529
|
+
const candidateStarted = Date.now();
|
|
530
|
+
const timings = {};
|
|
523
531
|
await runControl.waitIfPaused();
|
|
524
532
|
runControl.throwIfCanceled();
|
|
525
533
|
runControl.setPhase("recommend:candidate");
|
|
526
534
|
rootState = await ensureRecommendViewport(rootState, "candidate_loop");
|
|
527
535
|
|
|
528
|
-
const nextCandidateResult = await getNextInfiniteListCandidate({
|
|
536
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
529
537
|
client,
|
|
530
538
|
state: listState,
|
|
531
539
|
maxScrolls: listMaxScrolls,
|
|
@@ -552,7 +560,7 @@ export async function runRecommendWorkflow({
|
|
|
552
560
|
visible_index: visibleIndex
|
|
553
561
|
}
|
|
554
562
|
})
|
|
555
|
-
});
|
|
563
|
+
}));
|
|
556
564
|
if (!nextCandidateResult.ok) {
|
|
557
565
|
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
558
566
|
if (
|
|
@@ -644,11 +652,13 @@ export async function runRecommendWorkflow({
|
|
|
644
652
|
targetUrl,
|
|
645
653
|
maxAttempts: 2
|
|
646
654
|
});
|
|
655
|
+
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
656
|
+
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
647
657
|
cardNodeId = openedDetail.card_node_id || cardNodeId;
|
|
648
658
|
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
649
659
|
screeningCandidate = cardCandidate;
|
|
650
660
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
651
|
-
const networkWait = await waitForCvNetworkEvents(
|
|
661
|
+
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
652
662
|
waitForRecommendDetailNetworkEvents,
|
|
653
663
|
networkRecorder,
|
|
654
664
|
{
|
|
@@ -657,15 +667,21 @@ export async function runRecommendWorkflow({
|
|
|
657
667
|
requireLoaded: true,
|
|
658
668
|
intervalMs: 120
|
|
659
669
|
}
|
|
660
|
-
);
|
|
670
|
+
));
|
|
671
|
+
if (networkWait?.elapsed_ms != null) {
|
|
672
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
673
|
+
}
|
|
661
674
|
detailResult = await extractRecommendDetailCandidate(client, {
|
|
662
675
|
cardCandidate,
|
|
663
676
|
cardNodeId,
|
|
664
677
|
detailState: openedDetail.detail_state,
|
|
665
678
|
networkEvents: networkRecorder.events,
|
|
666
679
|
targetUrl,
|
|
667
|
-
closeDetail: false
|
|
680
|
+
closeDetail: false,
|
|
681
|
+
networkParseRetryMs: waitPlan.mode_before === "image" ? 500 : 2200,
|
|
682
|
+
networkParseIntervalMs: 250
|
|
668
683
|
});
|
|
684
|
+
addTiming(timings, "late_network_retry_ms", detailResult.network_parse_retry_elapsed_ms);
|
|
669
685
|
|
|
670
686
|
const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
|
|
671
687
|
let source = "network";
|
|
@@ -680,11 +696,25 @@ export async function runRecommendWorkflow({
|
|
|
680
696
|
|| openedDetail.detail_state?.resumeIframe?.node_id
|
|
681
697
|
|| null;
|
|
682
698
|
if (captureNodeId) {
|
|
683
|
-
imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
699
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
700
|
+
filePath: imageEvidenceFilePath({
|
|
701
|
+
imageOutputDir,
|
|
702
|
+
domain: "recommend",
|
|
703
|
+
runId: runControl?.runId,
|
|
704
|
+
index,
|
|
705
|
+
extension: "jpg"
|
|
706
|
+
}),
|
|
707
|
+
format: "jpeg",
|
|
708
|
+
quality: 72,
|
|
709
|
+
optimize: true,
|
|
710
|
+
resizeMaxWidth: 1100,
|
|
711
|
+
captureViewport: true,
|
|
684
712
|
padding: 4,
|
|
685
713
|
maxScreenshots: maxImagePages,
|
|
686
714
|
wheelDeltaY: imageWheelDeltaY,
|
|
687
|
-
settleMs:
|
|
715
|
+
settleMs: 350,
|
|
716
|
+
duplicateStopCount: 1,
|
|
717
|
+
skipDuplicateScreenshots: true,
|
|
688
718
|
metadata: {
|
|
689
719
|
domain: "recommend",
|
|
690
720
|
capture_mode: "scroll_sequence",
|
|
@@ -692,7 +722,7 @@ export async function runRecommendWorkflow({
|
|
|
692
722
|
run_candidate_index: index,
|
|
693
723
|
candidate_key: candidateKey
|
|
694
724
|
}
|
|
695
|
-
});
|
|
725
|
+
}));
|
|
696
726
|
source = "image";
|
|
697
727
|
recordCvImageFallback(cvAcquisitionState, {
|
|
698
728
|
parsedNetworkProfileCount,
|
|
@@ -730,7 +760,10 @@ export async function runRecommendWorkflow({
|
|
|
730
760
|
llmResult = createMissingLlmConfigResult();
|
|
731
761
|
} else {
|
|
732
762
|
try {
|
|
733
|
-
|
|
763
|
+
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
764
|
+
? "vision_model_ms"
|
|
765
|
+
: "text_model_ms";
|
|
766
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
734
767
|
candidate: screeningCandidate,
|
|
735
768
|
criteria,
|
|
736
769
|
config: llmConfig,
|
|
@@ -738,7 +771,7 @@ export async function runRecommendWorkflow({
|
|
|
738
771
|
imageEvidence: detailResult?.image_evidence || null,
|
|
739
772
|
maxImages: llmImageLimit,
|
|
740
773
|
imageDetail: llmImageDetail
|
|
741
|
-
});
|
|
774
|
+
}));
|
|
742
775
|
} catch (error) {
|
|
743
776
|
llmResult = createFailedLlmScreeningResult(error);
|
|
744
777
|
}
|
|
@@ -751,6 +784,7 @@ export async function runRecommendWorkflow({
|
|
|
751
784
|
let actionDiscovery = null;
|
|
752
785
|
let postActionResult = null;
|
|
753
786
|
if (postActionEnabled && detailResult) {
|
|
787
|
+
const postActionStarted = Date.now();
|
|
754
788
|
await runControl.waitIfPaused();
|
|
755
789
|
runControl.throwIfCanceled();
|
|
756
790
|
runControl.setPhase("recommend:post-action");
|
|
@@ -772,10 +806,12 @@ export async function runRecommendWorkflow({
|
|
|
772
806
|
if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
|
|
773
807
|
greetCount += 1;
|
|
774
808
|
}
|
|
809
|
+
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
775
810
|
}
|
|
776
811
|
if (detailResult && closeDetail) {
|
|
777
|
-
detailResult.close_result = await closeRecommendDetail(client);
|
|
812
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
778
813
|
}
|
|
814
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
779
815
|
const compactResult = {
|
|
780
816
|
index,
|
|
781
817
|
candidate_key: candidateKey,
|
|
@@ -785,7 +821,8 @@ export async function runRecommendWorkflow({
|
|
|
785
821
|
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
786
822
|
screening: compactScreening(screening),
|
|
787
823
|
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
788
|
-
post_action: postActionResult
|
|
824
|
+
post_action: postActionResult,
|
|
825
|
+
timings
|
|
789
826
|
};
|
|
790
827
|
results.push(compactResult);
|
|
791
828
|
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
@@ -816,6 +853,7 @@ export async function runRecommendWorkflow({
|
|
|
816
853
|
last_candidate_key: candidateKey,
|
|
817
854
|
last_score: screening.score
|
|
818
855
|
});
|
|
856
|
+
const checkpointStarted = Date.now();
|
|
819
857
|
runControl.checkpoint({
|
|
820
858
|
results,
|
|
821
859
|
last_candidate: {
|
|
@@ -831,6 +869,7 @@ export async function runRecommendWorkflow({
|
|
|
831
869
|
post_action: postActionResult
|
|
832
870
|
}
|
|
833
871
|
});
|
|
872
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
834
873
|
|
|
835
874
|
if (postActionResult?.stop_run) {
|
|
836
875
|
listEndReason = postActionResult.reason || "post_action_stop";
|
|
@@ -838,7 +877,10 @@ export async function runRecommendWorkflow({
|
|
|
838
877
|
}
|
|
839
878
|
|
|
840
879
|
if (delayMs > 0) {
|
|
880
|
+
const sleepStarted = Date.now();
|
|
841
881
|
await runControl.sleep(delayMs);
|
|
882
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
883
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
842
884
|
}
|
|
843
885
|
}
|
|
844
886
|
|
|
@@ -912,6 +954,7 @@ export function createRecommendRunService({
|
|
|
912
954
|
llmTimeoutMs = 120000,
|
|
913
955
|
llmImageLimit = 8,
|
|
914
956
|
llmImageDetail = "high",
|
|
957
|
+
imageOutputDir = "",
|
|
915
958
|
name = "recommend-domain-run"
|
|
916
959
|
} = {}) {
|
|
917
960
|
if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
|
|
@@ -955,7 +998,8 @@ export function createRecommendRunService({
|
|
|
955
998
|
llm_configured: Boolean(llmConfig),
|
|
956
999
|
llm_timeout_ms: llmTimeoutMs,
|
|
957
1000
|
llm_image_limit: llmImageLimit,
|
|
958
|
-
llm_image_detail: llmImageDetail
|
|
1001
|
+
llm_image_detail: llmImageDetail,
|
|
1002
|
+
image_output_dir: imageOutputDir || ""
|
|
959
1003
|
},
|
|
960
1004
|
progress: {
|
|
961
1005
|
card_count: 0,
|
|
@@ -1004,7 +1048,8 @@ export function createRecommendRunService({
|
|
|
1004
1048
|
llmConfig,
|
|
1005
1049
|
llmTimeoutMs,
|
|
1006
1050
|
llmImageLimit,
|
|
1007
|
-
llmImageDetail
|
|
1051
|
+
llmImageDetail,
|
|
1052
|
+
imageOutputDir
|
|
1008
1053
|
}, runControl)
|
|
1009
1054
|
});
|
|
1010
1055
|
}
|