@reconcrap/boss-recommend-mcp 2.0.6 → 2.0.8
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/README.md +2 -0
- package/package.json +1 -1
- package/src/chat-mcp.js +60 -18
- package/src/core/boss-cards/index.js +199 -0
- package/src/core/capture/index.js +4 -0
- package/src/core/cv-acquisition/index.js +1 -0
- package/src/core/reporting/legacy-csv.js +12 -3
- package/src/core/run/timing.js +33 -0
- package/src/core/screening/index.js +95 -5
- package/src/domains/chat/cards.js +9 -1
- package/src/domains/chat/run-service.js +142 -59
- package/src/domains/recommend/cards.js +16 -1
- package/src/domains/recommend/detail.js +11 -1
- package/src/domains/recommend/run-service.js +120 -13
- package/src/domains/recruit/cards.js +9 -1
- package/src/domains/recruit/detail.js +12 -1
- package/src/domains/recruit/run-service.js +127 -20
- package/src/index.js +50 -5
- package/src/recommend-mcp.js +82 -7
- package/src/recruit-mcp.js +104 -8
|
@@ -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";
|
|
@@ -21,7 +26,13 @@ import {
|
|
|
21
26
|
resetInfiniteListForRefreshRound
|
|
22
27
|
} from "../../core/infinite-list/index.js";
|
|
23
28
|
import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
24
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
callScreeningLlm,
|
|
31
|
+
compactScreeningLlmResult,
|
|
32
|
+
createFailedLlmScreeningResult,
|
|
33
|
+
llmResultToScreening,
|
|
34
|
+
screenCandidate
|
|
35
|
+
} from "../../core/screening/index.js";
|
|
25
36
|
import {
|
|
26
37
|
closeRecommendDetail,
|
|
27
38
|
createRecommendDetailNetworkRecorder,
|
|
@@ -165,10 +176,22 @@ function compactDetail(detailResult) {
|
|
|
165
176
|
parsed_network_profile_count: detailResult.parsed_network_profiles?.filter((item) => item.ok).length || 0,
|
|
166
177
|
cv_acquisition: detailResult.cv_acquisition || null,
|
|
167
178
|
image_evidence: summarizeImageEvidence(detailResult.image_evidence),
|
|
179
|
+
llm_screening: compactScreeningLlmResult(detailResult.llm_result),
|
|
168
180
|
close_result: detailResult.close_result
|
|
169
181
|
};
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
function normalizeScreeningMode(value) {
|
|
185
|
+
const normalized = String(value || "llm").trim().toLowerCase();
|
|
186
|
+
return ["deterministic", "local", "local_scorer"].includes(normalized)
|
|
187
|
+
? "deterministic"
|
|
188
|
+
: "llm";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createMissingLlmConfigResult() {
|
|
192
|
+
return createFailedLlmScreeningResult(new Error("LLM screening config is required for production recommend runs"));
|
|
193
|
+
}
|
|
194
|
+
|
|
172
195
|
function compactActionDiscovery(discovery) {
|
|
173
196
|
if (!discovery) return null;
|
|
174
197
|
return {
|
|
@@ -354,13 +377,21 @@ export async function runRecommendWorkflow({
|
|
|
354
377
|
executePostAction = true,
|
|
355
378
|
actionTimeoutMs = 8000,
|
|
356
379
|
actionIntervalMs = 500,
|
|
357
|
-
actionAfterClickDelayMs = 900
|
|
380
|
+
actionAfterClickDelayMs = 900,
|
|
381
|
+
screeningMode = "llm",
|
|
382
|
+
llmConfig = null,
|
|
383
|
+
llmTimeoutMs = 120000,
|
|
384
|
+
llmImageLimit = 8,
|
|
385
|
+
llmImageDetail = "high",
|
|
386
|
+
imageOutputDir = ""
|
|
358
387
|
} = {}, runControl) {
|
|
359
388
|
if (!client) throw new Error("runRecommendWorkflow requires a guarded CDP client");
|
|
360
389
|
const normalizedFilter = normalizeFilter(filter);
|
|
361
390
|
const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
|
|
362
391
|
const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
|
|
363
392
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
393
|
+
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
394
|
+
const useLlmScreening = normalizedScreeningMode !== "deterministic";
|
|
364
395
|
const postActionEnabled = normalizedPostAction !== "none";
|
|
365
396
|
const limit = Math.max(1, Number(maxCandidates) || 1);
|
|
366
397
|
const detailCountLimit = detailLimit == null ? limit : Math.max(0, Number(detailLimit) || 0);
|
|
@@ -484,6 +515,8 @@ export async function runRecommendWorkflow({
|
|
|
484
515
|
passed: 0,
|
|
485
516
|
greet_count: 0,
|
|
486
517
|
post_action_clicked: 0,
|
|
518
|
+
screening_mode: normalizedScreeningMode,
|
|
519
|
+
llm_screened: 0,
|
|
487
520
|
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
488
521
|
scroll_count: 0,
|
|
489
522
|
refresh_rounds: 0,
|
|
@@ -493,12 +526,14 @@ export async function runRecommendWorkflow({
|
|
|
493
526
|
});
|
|
494
527
|
|
|
495
528
|
while (results.length < limit) {
|
|
529
|
+
const candidateStarted = Date.now();
|
|
530
|
+
const timings = {};
|
|
496
531
|
await runControl.waitIfPaused();
|
|
497
532
|
runControl.throwIfCanceled();
|
|
498
533
|
runControl.setPhase("recommend:candidate");
|
|
499
534
|
rootState = await ensureRecommendViewport(rootState, "candidate_loop");
|
|
500
535
|
|
|
501
|
-
const nextCandidateResult = await getNextInfiniteListCandidate({
|
|
536
|
+
const nextCandidateResult = await measureTiming(timings, "card_read_ms", () => getNextInfiniteListCandidate({
|
|
502
537
|
client,
|
|
503
538
|
state: listState,
|
|
504
539
|
maxScrolls: listMaxScrolls,
|
|
@@ -525,7 +560,7 @@ export async function runRecommendWorkflow({
|
|
|
525
560
|
visible_index: visibleIndex
|
|
526
561
|
}
|
|
527
562
|
})
|
|
528
|
-
});
|
|
563
|
+
}));
|
|
529
564
|
if (!nextCandidateResult.ok) {
|
|
530
565
|
listEndReason = nextCandidateResult.reason || "list_exhausted";
|
|
531
566
|
if (
|
|
@@ -562,6 +597,7 @@ export async function runRecommendWorkflow({
|
|
|
562
597
|
screened: results.length,
|
|
563
598
|
detail_opened: results.filter((item) => item.detail).length,
|
|
564
599
|
passed: results.filter((item) => item.screening.passed).length,
|
|
600
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
565
601
|
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
566
602
|
scroll_count: compactInfiniteListState(listState).scroll_count,
|
|
567
603
|
refresh_rounds: refreshRounds,
|
|
@@ -616,11 +652,13 @@ export async function runRecommendWorkflow({
|
|
|
616
652
|
targetUrl,
|
|
617
653
|
maxAttempts: 2
|
|
618
654
|
});
|
|
655
|
+
addTiming(timings, "candidate_click_ms", openedDetail.timings?.candidate_click_ms);
|
|
656
|
+
addTiming(timings, "detail_open_ms", openedDetail.timings?.detail_open_ms);
|
|
619
657
|
cardNodeId = openedDetail.card_node_id || cardNodeId;
|
|
620
658
|
cardCandidate = openedDetail.card_candidate || cardCandidate;
|
|
621
659
|
screeningCandidate = cardCandidate;
|
|
622
660
|
const waitPlan = getCvNetworkWaitPlan(cvAcquisitionState);
|
|
623
|
-
const networkWait = await waitForCvNetworkEvents(
|
|
661
|
+
const networkWait = await measureTiming(timings, "network_cv_wait_ms", () => waitForCvNetworkEvents(
|
|
624
662
|
waitForRecommendDetailNetworkEvents,
|
|
625
663
|
networkRecorder,
|
|
626
664
|
{
|
|
@@ -629,7 +667,10 @@ export async function runRecommendWorkflow({
|
|
|
629
667
|
requireLoaded: true,
|
|
630
668
|
intervalMs: 120
|
|
631
669
|
}
|
|
632
|
-
);
|
|
670
|
+
));
|
|
671
|
+
if (networkWait?.elapsed_ms != null) {
|
|
672
|
+
timings.network_cv_wait_ms = Math.round(Number(networkWait.elapsed_ms) || 0);
|
|
673
|
+
}
|
|
633
674
|
detailResult = await extractRecommendDetailCandidate(client, {
|
|
634
675
|
cardCandidate,
|
|
635
676
|
cardNodeId,
|
|
@@ -652,7 +693,13 @@ export async function runRecommendWorkflow({
|
|
|
652
693
|
|| openedDetail.detail_state?.resumeIframe?.node_id
|
|
653
694
|
|| null;
|
|
654
695
|
if (captureNodeId) {
|
|
655
|
-
imageEvidence = await captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
696
|
+
imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
|
|
697
|
+
filePath: imageEvidenceFilePath({
|
|
698
|
+
imageOutputDir,
|
|
699
|
+
domain: "recommend",
|
|
700
|
+
runId: runControl?.runId,
|
|
701
|
+
index
|
|
702
|
+
}),
|
|
656
703
|
padding: 4,
|
|
657
704
|
maxScreenshots: maxImagePages,
|
|
658
705
|
wheelDeltaY: imageWheelDeltaY,
|
|
@@ -664,7 +711,7 @@ export async function runRecommendWorkflow({
|
|
|
664
711
|
run_candidate_index: index,
|
|
665
712
|
candidate_key: candidateKey
|
|
666
713
|
}
|
|
667
|
-
});
|
|
714
|
+
}));
|
|
668
715
|
source = "image";
|
|
669
716
|
recordCvImageFallback(cvAcquisitionState, {
|
|
670
717
|
parsedNetworkProfileCount,
|
|
@@ -696,10 +743,37 @@ export async function runRecommendWorkflow({
|
|
|
696
743
|
await runControl.waitIfPaused();
|
|
697
744
|
runControl.throwIfCanceled();
|
|
698
745
|
runControl.setPhase("recommend:screening");
|
|
699
|
-
|
|
746
|
+
let llmResult = null;
|
|
747
|
+
if (useLlmScreening) {
|
|
748
|
+
if (!llmConfig) {
|
|
749
|
+
llmResult = createMissingLlmConfigResult();
|
|
750
|
+
} else {
|
|
751
|
+
try {
|
|
752
|
+
const llmTimingKey = detailResult?.image_evidence?.file_paths?.length
|
|
753
|
+
? "vision_model_ms"
|
|
754
|
+
: "text_model_ms";
|
|
755
|
+
llmResult = await measureTiming(timings, llmTimingKey, () => callScreeningLlm({
|
|
756
|
+
candidate: screeningCandidate,
|
|
757
|
+
criteria,
|
|
758
|
+
config: llmConfig,
|
|
759
|
+
timeoutMs: llmTimeoutMs,
|
|
760
|
+
imageEvidence: detailResult?.image_evidence || null,
|
|
761
|
+
maxImages: llmImageLimit,
|
|
762
|
+
imageDetail: llmImageDetail
|
|
763
|
+
}));
|
|
764
|
+
} catch (error) {
|
|
765
|
+
llmResult = createFailedLlmScreeningResult(error);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (detailResult) detailResult.llm_result = llmResult;
|
|
769
|
+
}
|
|
770
|
+
const screening = useLlmScreening
|
|
771
|
+
? llmResultToScreening(llmResult, screeningCandidate)
|
|
772
|
+
: screenCandidate(screeningCandidate, { criteria });
|
|
700
773
|
let actionDiscovery = null;
|
|
701
774
|
let postActionResult = null;
|
|
702
775
|
if (postActionEnabled && detailResult) {
|
|
776
|
+
const postActionStarted = Date.now();
|
|
703
777
|
await runControl.waitIfPaused();
|
|
704
778
|
runControl.throwIfCanceled();
|
|
705
779
|
runControl.setPhase("recommend:post-action");
|
|
@@ -721,19 +795,23 @@ export async function runRecommendWorkflow({
|
|
|
721
795
|
if (postActionResult.counted_as_greet && postActionResult.action_clicked) {
|
|
722
796
|
greetCount += 1;
|
|
723
797
|
}
|
|
798
|
+
addTiming(timings, "post_action_ms", Date.now() - postActionStarted);
|
|
724
799
|
}
|
|
725
800
|
if (detailResult && closeDetail) {
|
|
726
|
-
detailResult.close_result = await closeRecommendDetail(client);
|
|
801
|
+
detailResult.close_result = await measureTiming(timings, "close_detail_ms", () => closeRecommendDetail(client));
|
|
727
802
|
}
|
|
803
|
+
timings.total_ms = Date.now() - candidateStarted;
|
|
728
804
|
const compactResult = {
|
|
729
805
|
index,
|
|
730
806
|
candidate_key: candidateKey,
|
|
731
807
|
card_node_id: cardNodeId,
|
|
732
808
|
candidate: compactCandidate(screeningCandidate),
|
|
733
809
|
detail: compactDetail(detailResult),
|
|
810
|
+
llm_screening: detailResult ? null : compactScreeningLlmResult(llmResult),
|
|
734
811
|
screening: compactScreening(screening),
|
|
735
812
|
action_discovery: compactActionDiscovery(actionDiscovery),
|
|
736
|
-
post_action: postActionResult
|
|
813
|
+
post_action: postActionResult,
|
|
814
|
+
timings
|
|
737
815
|
};
|
|
738
816
|
results.push(compactResult);
|
|
739
817
|
markInfiniteListCandidateProcessed(listState, candidateKey, {
|
|
@@ -750,6 +828,7 @@ export async function runRecommendWorkflow({
|
|
|
750
828
|
screened: results.length,
|
|
751
829
|
detail_opened: results.filter((item) => item.detail).length,
|
|
752
830
|
passed: results.filter((item) => item.screening.passed).length,
|
|
831
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
753
832
|
greet_count: greetCount,
|
|
754
833
|
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
755
834
|
unique_seen: compactInfiniteListState(listState).seen_count,
|
|
@@ -763,7 +842,9 @@ export async function runRecommendWorkflow({
|
|
|
763
842
|
last_candidate_key: candidateKey,
|
|
764
843
|
last_score: screening.score
|
|
765
844
|
});
|
|
845
|
+
const checkpointStarted = Date.now();
|
|
766
846
|
runControl.checkpoint({
|
|
847
|
+
results,
|
|
767
848
|
last_candidate: {
|
|
768
849
|
id: screeningCandidate.id || null,
|
|
769
850
|
key: candidateKey,
|
|
@@ -773,9 +854,11 @@ export async function runRecommendWorkflow({
|
|
|
773
854
|
passed: screening.passed,
|
|
774
855
|
score: screening.score
|
|
775
856
|
},
|
|
857
|
+
llm_screening: compactScreeningLlmResult(llmResult),
|
|
776
858
|
post_action: postActionResult
|
|
777
859
|
}
|
|
778
860
|
});
|
|
861
|
+
addTiming(compactResult.timings, "checkpoint_save_ms", Date.now() - checkpointStarted);
|
|
779
862
|
|
|
780
863
|
if (postActionResult?.stop_run) {
|
|
781
864
|
listEndReason = postActionResult.reason || "post_action_stop";
|
|
@@ -783,7 +866,10 @@ export async function runRecommendWorkflow({
|
|
|
783
866
|
}
|
|
784
867
|
|
|
785
868
|
if (delayMs > 0) {
|
|
869
|
+
const sleepStarted = Date.now();
|
|
786
870
|
await runControl.sleep(delayMs);
|
|
871
|
+
addTiming(compactResult.timings, "sleep_ms", Date.now() - sleepStarted);
|
|
872
|
+
compactResult.timings.total_ms = Date.now() - candidateStarted;
|
|
787
873
|
}
|
|
788
874
|
}
|
|
789
875
|
|
|
@@ -806,6 +892,7 @@ export async function runRecommendWorkflow({
|
|
|
806
892
|
processed: results.length,
|
|
807
893
|
screened: results.length,
|
|
808
894
|
detail_opened: results.filter((item) => item.detail).length,
|
|
895
|
+
llm_screened: results.filter((item) => item.detail?.llm_screening || item.llm_screening).length,
|
|
809
896
|
passed: results.filter((item) => item.screening.passed).length,
|
|
810
897
|
greet_count: greetCount,
|
|
811
898
|
post_action_clicked: results.filter((item) => item.post_action?.action_clicked).length,
|
|
@@ -851,6 +938,12 @@ export function createRecommendRunService({
|
|
|
851
938
|
actionTimeoutMs = 8000,
|
|
852
939
|
actionIntervalMs = 500,
|
|
853
940
|
actionAfterClickDelayMs = 900,
|
|
941
|
+
screeningMode = "llm",
|
|
942
|
+
llmConfig = null,
|
|
943
|
+
llmTimeoutMs = 120000,
|
|
944
|
+
llmImageLimit = 8,
|
|
945
|
+
llmImageDetail = "high",
|
|
946
|
+
imageOutputDir = "",
|
|
854
947
|
name = "recommend-domain-run"
|
|
855
948
|
} = {}) {
|
|
856
949
|
if (!client) throw new Error("startRecommendRun requires a guarded CDP client");
|
|
@@ -858,6 +951,7 @@ export function createRecommendRunService({
|
|
|
858
951
|
const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
|
|
859
952
|
const requestedPageScope = normalizeRecommendPageScope(pageScope) || "recommend";
|
|
860
953
|
const normalizedFallbackPageScope = normalizeRecommendPageScope(fallbackPageScope) || "recommend";
|
|
954
|
+
const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
|
|
861
955
|
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
862
956
|
const normalizedDetailLimit = detailLimit == null ? candidateLimit : Math.max(0, Number(detailLimit) || 0);
|
|
863
957
|
return manager.startRun({
|
|
@@ -888,7 +982,13 @@ export function createRecommendRunService({
|
|
|
888
982
|
post_action: normalizedPostAction,
|
|
889
983
|
max_greet_count: Number.isInteger(maxGreetCount) ? maxGreetCount : null,
|
|
890
984
|
execute_post_action: Boolean(executePostAction),
|
|
891
|
-
action_timeout_ms: actionTimeoutMs
|
|
985
|
+
action_timeout_ms: actionTimeoutMs,
|
|
986
|
+
screening_mode: normalizedScreeningMode,
|
|
987
|
+
llm_configured: Boolean(llmConfig),
|
|
988
|
+
llm_timeout_ms: llmTimeoutMs,
|
|
989
|
+
llm_image_limit: llmImageLimit,
|
|
990
|
+
llm_image_detail: llmImageDetail,
|
|
991
|
+
image_output_dir: imageOutputDir || ""
|
|
892
992
|
},
|
|
893
993
|
progress: {
|
|
894
994
|
card_count: 0,
|
|
@@ -896,6 +996,7 @@ export function createRecommendRunService({
|
|
|
896
996
|
processed: 0,
|
|
897
997
|
screened: 0,
|
|
898
998
|
detail_opened: 0,
|
|
999
|
+
llm_screened: 0,
|
|
899
1000
|
passed: 0,
|
|
900
1001
|
greet_count: 0,
|
|
901
1002
|
post_action_clicked: 0
|
|
@@ -931,7 +1032,13 @@ export function createRecommendRunService({
|
|
|
931
1032
|
executePostAction,
|
|
932
1033
|
actionTimeoutMs,
|
|
933
1034
|
actionIntervalMs,
|
|
934
|
-
actionAfterClickDelayMs
|
|
1035
|
+
actionAfterClickDelayMs,
|
|
1036
|
+
screeningMode: normalizedScreeningMode,
|
|
1037
|
+
llmConfig,
|
|
1038
|
+
llmTimeoutMs,
|
|
1039
|
+
llmImageLimit,
|
|
1040
|
+
llmImageDetail,
|
|
1041
|
+
imageOutputDir
|
|
935
1042
|
}, runControl)
|
|
936
1043
|
});
|
|
937
1044
|
}
|
|
@@ -4,9 +4,16 @@ import {
|
|
|
4
4
|
querySelectorAll,
|
|
5
5
|
sleep
|
|
6
6
|
} from "../../core/browser/index.js";
|
|
7
|
+
import { mergeBossCandidateCardFields } from "../../core/boss-cards/index.js";
|
|
7
8
|
import { normalizeCandidateFromHtml } from "../../core/screening/index.js";
|
|
8
9
|
import { RECRUIT_CARD_SELECTOR } from "./constants.js";
|
|
9
10
|
|
|
11
|
+
function mergeRecruitCardFields(candidate, outerHTML = "") {
|
|
12
|
+
return mergeBossCandidateCardFields(candidate, outerHTML, {
|
|
13
|
+
metadataKey: "search_card_fields"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export async function findRecruitCardNodeIds(client, frameNodeId, {
|
|
11
18
|
selector = RECRUIT_CARD_SELECTOR
|
|
12
19
|
} = {}) {
|
|
@@ -37,7 +44,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
|
|
|
37
44
|
getAttributesMap(client, cardNodeId),
|
|
38
45
|
getOuterHTML(client, cardNodeId)
|
|
39
46
|
]);
|
|
40
|
-
|
|
47
|
+
const candidate = normalizeCandidateFromHtml({
|
|
41
48
|
domain: "recruit",
|
|
42
49
|
source,
|
|
43
50
|
html: outerHTML,
|
|
@@ -48,6 +55,7 @@ export async function readRecruitCardCandidate(client, cardNodeId, {
|
|
|
48
55
|
...metadata
|
|
49
56
|
}
|
|
50
57
|
});
|
|
58
|
+
return mergeRecruitCardFields(candidate, outerHTML);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
export async function readFirstRecruitCardCandidate(client, frameNodeId, options = {}) {
|
|
@@ -213,17 +213,22 @@ export async function waitForRecruitDetailContent(client, {
|
|
|
213
213
|
export async function openRecruitCardDetail(client, cardNodeId, {
|
|
214
214
|
timeoutMs = 12000
|
|
215
215
|
} = {}) {
|
|
216
|
+
const openedStarted = Date.now();
|
|
216
217
|
const attempts = [];
|
|
218
|
+
const clickStarted = Date.now();
|
|
217
219
|
const cardBox = await clickNodeCenter(client, cardNodeId, {
|
|
218
220
|
scrollIntoView: true
|
|
219
221
|
});
|
|
222
|
+
let candidateClickMs = Date.now() - clickStarted;
|
|
220
223
|
attempts.push({
|
|
221
224
|
mode: "card-center",
|
|
222
225
|
center: cardBox.center
|
|
223
226
|
});
|
|
227
|
+
const detailStarted = Date.now();
|
|
224
228
|
let detailState = await waitForRecruitDetail(client, { timeoutMs });
|
|
225
229
|
|
|
226
230
|
if (!detailState?.popup && !detailState?.resumeIframe) {
|
|
231
|
+
const fallbackClickStarted = Date.now();
|
|
227
232
|
const leftTitlePoint = {
|
|
228
233
|
x: cardBox.rect.x + Math.min(140, Math.max(40, cardBox.rect.width * 0.2)),
|
|
229
234
|
y: cardBox.rect.y + Math.min(42, Math.max(24, cardBox.rect.height * 0.28))
|
|
@@ -232,6 +237,7 @@ export async function openRecruitCardDetail(client, cardNodeId, {
|
|
|
232
237
|
clickCount: 2,
|
|
233
238
|
delayMs: 120
|
|
234
239
|
});
|
|
240
|
+
candidateClickMs += Date.now() - fallbackClickStarted;
|
|
235
241
|
attempts.push({
|
|
236
242
|
mode: "card-left-title-double-click",
|
|
237
243
|
center: leftTitlePoint
|
|
@@ -248,7 +254,12 @@ export async function openRecruitCardDetail(client, cardNodeId, {
|
|
|
248
254
|
return {
|
|
249
255
|
card_box: cardBox,
|
|
250
256
|
open_attempts: attempts,
|
|
251
|
-
detail_state: detailState
|
|
257
|
+
detail_state: detailState,
|
|
258
|
+
timings: {
|
|
259
|
+
candidate_click_ms: candidateClickMs,
|
|
260
|
+
detail_open_ms: Date.now() - detailStarted,
|
|
261
|
+
open_total_ms: Date.now() - openedStarted
|
|
262
|
+
}
|
|
252
263
|
};
|
|
253
264
|
}
|
|
254
265
|
|