@reconcrap/boss-recommend-mcp 2.0.54 → 2.0.55
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/constants.js +9 -24
- package/src/domains/chat/detail.js +34 -186
- package/src/domains/common/account-rights-panel.js +314 -0
- package/src/domains/recommend/detail.js +24 -9
- package/src/domains/recommend/run-service.js +122 -57
- package/src/domains/recruit/detail.js +15 -0
- package/src/domains/recruit/run-service.js +78 -1
package/package.json
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS,
|
|
3
|
+
BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES
|
|
4
|
+
} from "../common/account-rights-panel.js";
|
|
5
|
+
|
|
6
|
+
export const CHAT_TARGET_URL = "https://www.zhipin.com/web/chat/index";
|
|
2
7
|
|
|
3
8
|
export const CHAT_CARD_SELECTORS = Object.freeze([
|
|
4
9
|
".geek-item[data-id]",
|
|
@@ -208,29 +213,9 @@ export const CHAT_RESUME_CLOSE_SELECTORS = Object.freeze([
|
|
|
208
213
|
'[title*="关闭"]'
|
|
209
214
|
]);
|
|
210
215
|
|
|
211
|
-
export const CHAT_BLOCKING_PANEL_TEXT_QUERIES =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"全部账号权益使用量",
|
|
215
|
-
"职位发布权益总量",
|
|
216
|
-
"每日使用权益总量"
|
|
217
|
-
]);
|
|
218
|
-
|
|
219
|
-
export const CHAT_BLOCKING_PANEL_CLOSE_SELECTORS = Object.freeze([
|
|
220
|
-
".boss-popup__close",
|
|
221
|
-
".boss-dialog__close",
|
|
222
|
-
".side-panel-close",
|
|
223
|
-
".drawer-close",
|
|
224
|
-
".panel-close",
|
|
225
|
-
".popup-close",
|
|
226
|
-
".modal-close",
|
|
227
|
-
".dialog-close",
|
|
228
|
-
".close-btn",
|
|
229
|
-
".icon-close",
|
|
230
|
-
"[class*=\"close\"]",
|
|
231
|
-
'[aria-label*="关闭"]',
|
|
232
|
-
'[title*="关闭"]'
|
|
233
|
-
]);
|
|
216
|
+
export const CHAT_BLOCKING_PANEL_TEXT_QUERIES = BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES;
|
|
217
|
+
|
|
218
|
+
export const CHAT_BLOCKING_PANEL_CLOSE_SELECTORS = BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS;
|
|
234
219
|
|
|
235
220
|
export const CHAT_PROFILE_NETWORK_PATTERNS = Object.freeze([
|
|
236
221
|
/\/wapi\/zpjob\/view\/geek\/info(?:\/v2)?\b/i,
|
|
@@ -12,13 +12,17 @@ import {
|
|
|
12
12
|
querySelectorAll,
|
|
13
13
|
sleep
|
|
14
14
|
} from "../../core/browser/index.js";
|
|
15
|
-
import {
|
|
16
|
-
buildScreeningCandidateFromDetail,
|
|
17
|
-
htmlToText
|
|
18
|
-
} from "../../core/screening/index.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
import {
|
|
16
|
+
buildScreeningCandidateFromDetail,
|
|
17
|
+
htmlToText
|
|
18
|
+
} from "../../core/screening/index.js";
|
|
19
|
+
import {
|
|
20
|
+
closeBossAccountRightsBlockingPanel,
|
|
21
|
+
findBossAccountRightsBlockingPanel
|
|
22
|
+
} from "../common/account-rights-panel.js";
|
|
23
|
+
import {
|
|
24
|
+
CHAT_ACTIVE_CANDIDATE_SELECTORS,
|
|
25
|
+
CHAT_ASK_RESUME_BUTTON_SELECTORS,
|
|
22
26
|
CHAT_ATTACHMENT_RESUME_BUTTON_SELECTORS,
|
|
23
27
|
CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
|
|
24
28
|
CHAT_BLOCKING_PANEL_TEXT_QUERIES,
|
|
@@ -735,185 +739,29 @@ export async function quickChatResumeModalOpenProbe(client, {
|
|
|
735
739
|
};
|
|
736
740
|
}
|
|
737
741
|
|
|
738
|
-
async function
|
|
739
|
-
|
|
740
|
-
} = {}) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
} catch {
|
|
762
|
-
return [];
|
|
763
|
-
} finally {
|
|
764
|
-
if (searchId && typeof client?.DOM?.discardSearchResults === "function") {
|
|
765
|
-
try {
|
|
766
|
-
await client.DOM.discardSearchResults({ searchId });
|
|
767
|
-
} catch {
|
|
768
|
-
// Best-effort cleanup only.
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
const firstPass = await searchOnce();
|
|
775
|
-
if (!firstPass.length) return [];
|
|
776
|
-
const firstPassNodeIds = firstPass.filter((nodeId) => Number(nodeId) > 0);
|
|
777
|
-
if (firstPassNodeIds.length || typeof client?.DOM?.getDocument !== "function") return firstPassNodeIds;
|
|
778
|
-
try {
|
|
779
|
-
await client.DOM.getDocument({
|
|
780
|
-
depth: -1,
|
|
781
|
-
pierce: true
|
|
782
|
-
});
|
|
783
|
-
} catch {
|
|
784
|
-
return firstPassNodeIds;
|
|
785
|
-
}
|
|
786
|
-
return (await searchOnce()).filter((nodeId) => Number(nodeId) > 0);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
export async function findChatBlockingPanel(client, {
|
|
790
|
-
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
791
|
-
} = {}) {
|
|
792
|
-
for (const query of textQueries || []) {
|
|
793
|
-
const nodeIds = await performDomTextSearch(client, query);
|
|
794
|
-
for (const nodeId of nodeIds) {
|
|
795
|
-
try {
|
|
796
|
-
const box = await getNodeBox(client, nodeId);
|
|
797
|
-
if (box?.rect?.width > 2 && box?.rect?.height > 2) {
|
|
798
|
-
return {
|
|
799
|
-
open: true,
|
|
800
|
-
reason: "blocking_panel_text_visible",
|
|
801
|
-
query,
|
|
802
|
-
node_id: nodeId,
|
|
803
|
-
rect: box.rect,
|
|
804
|
-
center: box.center
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
} catch {
|
|
808
|
-
// Hidden or stale text hits are ignored.
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return {
|
|
813
|
-
open: false
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function blockingPanelOutsideClickPoint(probe = {}) {
|
|
818
|
-
const rect = probe?.rect || {};
|
|
819
|
-
// Click the empty lower-left sidebar area. It is outside the rights drawer
|
|
820
|
-
// and avoids top nav, chat rows, message controls, and candidate actions.
|
|
821
|
-
const x = 84;
|
|
822
|
-
const y = Number.isFinite(Number(rect.y))
|
|
823
|
-
? Math.max(560, Math.min(680, Number(rect.y) + 600))
|
|
824
|
-
: 660;
|
|
825
|
-
return {
|
|
826
|
-
x,
|
|
827
|
-
y,
|
|
828
|
-
mode: "empty-lower-left-sidebar"
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
export async function closeChatBlockingPanels(client, {
|
|
833
|
-
attemptsLimit = 2
|
|
834
|
-
} = {}) {
|
|
835
|
-
const attempts = [];
|
|
836
|
-
let probe = await findChatBlockingPanel(client);
|
|
837
|
-
if (!probe.open) {
|
|
838
|
-
return {
|
|
839
|
-
closed: true,
|
|
840
|
-
already_closed: true,
|
|
841
|
-
probe,
|
|
842
|
-
attempts
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
847
|
-
const outsidePoint = blockingPanelOutsideClickPoint(probe);
|
|
848
|
-
try {
|
|
849
|
-
await clickPoint(client, outsidePoint.x, outsidePoint.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
850
|
-
attempts.push({
|
|
851
|
-
mode: "outside-click",
|
|
852
|
-
point: outsidePoint
|
|
853
|
-
});
|
|
854
|
-
} catch (error) {
|
|
855
|
-
attempts.push({
|
|
856
|
-
mode: "outside-click-error",
|
|
857
|
-
point: outsidePoint,
|
|
858
|
-
error: error?.message || String(error)
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
await sleep(700);
|
|
862
|
-
|
|
863
|
-
probe = await findChatBlockingPanel(client);
|
|
864
|
-
if (!probe.open) {
|
|
865
|
-
return {
|
|
866
|
-
closed: true,
|
|
867
|
-
already_closed: false,
|
|
868
|
-
probe,
|
|
869
|
-
attempts
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const rootState = await getChatRoots(client);
|
|
874
|
-
const closeTarget = await findVisibleTarget(client, rootState.roots, CHAT_BLOCKING_PANEL_CLOSE_SELECTORS);
|
|
875
|
-
if (closeTarget) {
|
|
876
|
-
try {
|
|
877
|
-
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
878
|
-
attempts.push({
|
|
879
|
-
mode: "close-selector",
|
|
880
|
-
selector: closeTarget.selector,
|
|
881
|
-
root: closeTarget.root
|
|
882
|
-
});
|
|
883
|
-
} catch (error) {
|
|
884
|
-
attempts.push({
|
|
885
|
-
mode: "close-selector-error",
|
|
886
|
-
selector: closeTarget.selector,
|
|
887
|
-
root: closeTarget.root,
|
|
888
|
-
error: error?.message || String(error)
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
} else {
|
|
892
|
-
await pressEscape(client);
|
|
893
|
-
attempts.push({ mode: "Escape" });
|
|
894
|
-
}
|
|
895
|
-
await sleep(700);
|
|
896
|
-
|
|
897
|
-
probe = await findChatBlockingPanel(client);
|
|
898
|
-
if (!probe.open) {
|
|
899
|
-
return {
|
|
900
|
-
closed: true,
|
|
901
|
-
already_closed: false,
|
|
902
|
-
probe,
|
|
903
|
-
attempts
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
return {
|
|
909
|
-
closed: false,
|
|
910
|
-
already_closed: false,
|
|
911
|
-
probe,
|
|
912
|
-
attempts
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
export async function readChatResumeHtml(client, resumeState) {
|
|
742
|
+
export async function findChatBlockingPanel(client, {
|
|
743
|
+
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
744
|
+
} = {}) {
|
|
745
|
+
const panel = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
746
|
+
return panel.open
|
|
747
|
+
? { ...panel, reason: "blocking_panel_text_visible" }
|
|
748
|
+
: panel;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export async function closeChatBlockingPanels(client, {
|
|
752
|
+
attemptsLimit = 2,
|
|
753
|
+
closeSelectors = CHAT_BLOCKING_PANEL_CLOSE_SELECTORS,
|
|
754
|
+
textQueries = CHAT_BLOCKING_PANEL_TEXT_QUERIES
|
|
755
|
+
} = {}) {
|
|
756
|
+
return closeBossAccountRightsBlockingPanel(client, {
|
|
757
|
+
attemptsLimit,
|
|
758
|
+
closeSelectors,
|
|
759
|
+
resolveRoots: getChatRoots,
|
|
760
|
+
textQueries
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export async function readChatResumeHtml(client, resumeState) {
|
|
917
765
|
let popupHTML = "";
|
|
918
766
|
let contentHTML = "";
|
|
919
767
|
let resumeIframeHTML = "";
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clickPoint,
|
|
3
|
+
DETERMINISTIC_CLICK_OPTIONS,
|
|
4
|
+
getDocumentRoot,
|
|
5
|
+
getNodeBox,
|
|
6
|
+
pressKey,
|
|
7
|
+
querySelectorAll,
|
|
8
|
+
sleep
|
|
9
|
+
} from "../../core/browser/index.js";
|
|
10
|
+
|
|
11
|
+
export const BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES = Object.freeze([
|
|
12
|
+
"我的权益",
|
|
13
|
+
"VVIP账号-精选版专享权益",
|
|
14
|
+
"全部账号权益使用量",
|
|
15
|
+
"职位发布权益总量",
|
|
16
|
+
"每日使用权益总量"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS = Object.freeze([
|
|
20
|
+
".boss-popup__close",
|
|
21
|
+
".boss-dialog__close",
|
|
22
|
+
".side-panel-close",
|
|
23
|
+
".drawer-close",
|
|
24
|
+
".panel-close",
|
|
25
|
+
".popup-close",
|
|
26
|
+
".modal-close",
|
|
27
|
+
".dialog-close",
|
|
28
|
+
".close-btn",
|
|
29
|
+
".icon-close",
|
|
30
|
+
"[class*=\"close\"]",
|
|
31
|
+
'[aria-label*="关闭"]',
|
|
32
|
+
'[title*="关闭"]'
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
async function performDomTextSearch(client, query, {
|
|
36
|
+
limit = 6
|
|
37
|
+
} = {}) {
|
|
38
|
+
if (typeof client?.DOM?.performSearch !== "function"
|
|
39
|
+
|| typeof client?.DOM?.getSearchResults !== "function") {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const searchOnce = async () => {
|
|
44
|
+
let searchId = "";
|
|
45
|
+
try {
|
|
46
|
+
const search = await client.DOM.performSearch({
|
|
47
|
+
query,
|
|
48
|
+
includeUserAgentShadowDOM: true
|
|
49
|
+
});
|
|
50
|
+
searchId = search?.searchId || "";
|
|
51
|
+
const resultCount = Math.min(Number(search?.resultCount) || 0, Math.max(0, Number(limit) || 0));
|
|
52
|
+
if (!searchId || resultCount <= 0) return [];
|
|
53
|
+
const results = await client.DOM.getSearchResults({
|
|
54
|
+
searchId,
|
|
55
|
+
fromIndex: 0,
|
|
56
|
+
toIndex: resultCount
|
|
57
|
+
});
|
|
58
|
+
return results?.nodeIds || [];
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
} finally {
|
|
62
|
+
if (searchId && typeof client?.DOM?.discardSearchResults === "function") {
|
|
63
|
+
try {
|
|
64
|
+
await client.DOM.discardSearchResults({ searchId });
|
|
65
|
+
} catch {
|
|
66
|
+
// Best-effort cleanup only.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const firstPass = await searchOnce();
|
|
73
|
+
if (!firstPass.length) return [];
|
|
74
|
+
const firstPassNodeIds = firstPass.filter((nodeId) => Number(nodeId) > 0);
|
|
75
|
+
if (firstPassNodeIds.length || typeof client?.DOM?.getDocument !== "function") return firstPassNodeIds;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await client.DOM.getDocument({
|
|
79
|
+
depth: -1,
|
|
80
|
+
pierce: true
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
return firstPassNodeIds;
|
|
84
|
+
}
|
|
85
|
+
return (await searchOnce()).filter((nodeId) => Number(nodeId) > 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function findBossAccountRightsBlockingPanel(client, {
|
|
89
|
+
textQueries = BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES
|
|
90
|
+
} = {}) {
|
|
91
|
+
for (const query of textQueries || []) {
|
|
92
|
+
const nodeIds = await performDomTextSearch(client, query);
|
|
93
|
+
for (const nodeId of nodeIds) {
|
|
94
|
+
try {
|
|
95
|
+
const box = await getNodeBox(client, nodeId);
|
|
96
|
+
if (box?.rect?.width > 2 && box?.rect?.height > 2) {
|
|
97
|
+
return {
|
|
98
|
+
open: true,
|
|
99
|
+
reason: "account_rights_panel_text_visible",
|
|
100
|
+
query,
|
|
101
|
+
node_id: nodeId,
|
|
102
|
+
rect: box.rect,
|
|
103
|
+
center: box.center
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Hidden or stale text hits are ignored.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
open: false
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function accountRightsPanelOutsideClickPoint(probe = {}) {
|
|
117
|
+
const rect = probe?.rect || {};
|
|
118
|
+
// Click the empty lower-left sidebar area. It is outside the rights drawer
|
|
119
|
+
// and avoids top nav, chat rows, message controls, and candidate actions.
|
|
120
|
+
const x = 84;
|
|
121
|
+
const y = Number.isFinite(Number(rect.y))
|
|
122
|
+
? Math.max(560, Math.min(680, Number(rect.y) + 600))
|
|
123
|
+
: 660;
|
|
124
|
+
return {
|
|
125
|
+
x,
|
|
126
|
+
y,
|
|
127
|
+
mode: "empty-lower-left-sidebar"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function resolveBlockingPanelRoots(client, {
|
|
132
|
+
roots = null,
|
|
133
|
+
rootState = null,
|
|
134
|
+
resolveRoots = null
|
|
135
|
+
} = {}) {
|
|
136
|
+
if (Array.isArray(roots) && roots.some((root) => root?.nodeId)) {
|
|
137
|
+
return roots.filter((root) => root?.nodeId);
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(rootState?.roots) && rootState.roots.some((root) => root?.nodeId)) {
|
|
140
|
+
return rootState.roots.filter((root) => root?.nodeId);
|
|
141
|
+
}
|
|
142
|
+
if (typeof resolveRoots === "function") {
|
|
143
|
+
try {
|
|
144
|
+
const resolved = await resolveRoots(client);
|
|
145
|
+
if (Array.isArray(resolved)) return resolved.filter((root) => root?.nodeId);
|
|
146
|
+
if (Array.isArray(resolved?.roots)) return resolved.roots.filter((root) => root?.nodeId);
|
|
147
|
+
} catch {
|
|
148
|
+
// Fall through to the top document. The rights panel lives there.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const topRoot = await getDocumentRoot(client);
|
|
153
|
+
return [{ name: "top", nodeId: topRoot.nodeId }];
|
|
154
|
+
} catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function findVisibleCloseTarget(client, roots, selectors) {
|
|
160
|
+
let fallback = null;
|
|
161
|
+
for (const root of roots || []) {
|
|
162
|
+
if (!root?.nodeId) continue;
|
|
163
|
+
for (const selector of selectors || []) {
|
|
164
|
+
let nodeIds = [];
|
|
165
|
+
try {
|
|
166
|
+
nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
167
|
+
} catch {
|
|
168
|
+
nodeIds = [];
|
|
169
|
+
}
|
|
170
|
+
for (const nodeId of nodeIds) {
|
|
171
|
+
const target = {
|
|
172
|
+
root: root.name,
|
|
173
|
+
root_node_id: root.nodeId,
|
|
174
|
+
selector,
|
|
175
|
+
node_id: nodeId
|
|
176
|
+
};
|
|
177
|
+
if (!fallback) fallback = target;
|
|
178
|
+
try {
|
|
179
|
+
const box = await getNodeBox(client, nodeId);
|
|
180
|
+
if (box?.rect?.width > 2 && box?.rect?.height > 2) {
|
|
181
|
+
return {
|
|
182
|
+
...target,
|
|
183
|
+
center: box.center,
|
|
184
|
+
rect: box.rect
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Stale close candidates are ignored.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return fallback;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function pressEscape(client) {
|
|
197
|
+
await pressKey(client, "Escape", {
|
|
198
|
+
code: "Escape",
|
|
199
|
+
windowsVirtualKeyCode: 27,
|
|
200
|
+
nativeVirtualKeyCode: 27
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function closeBossAccountRightsBlockingPanel(client, {
|
|
205
|
+
attemptsLimit = 2,
|
|
206
|
+
closeSelectors = BOSS_ACCOUNT_RIGHTS_PANEL_CLOSE_SELECTORS,
|
|
207
|
+
resolveRoots = null,
|
|
208
|
+
roots = null,
|
|
209
|
+
rootState = null,
|
|
210
|
+
textQueries = BOSS_ACCOUNT_RIGHTS_PANEL_TEXT_QUERIES,
|
|
211
|
+
waitMs = 700
|
|
212
|
+
} = {}) {
|
|
213
|
+
const attempts = [];
|
|
214
|
+
let probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
215
|
+
if (!probe.open) {
|
|
216
|
+
return {
|
|
217
|
+
closed: true,
|
|
218
|
+
already_closed: true,
|
|
219
|
+
probe,
|
|
220
|
+
attempts
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
225
|
+
const outsidePoint = accountRightsPanelOutsideClickPoint(probe);
|
|
226
|
+
try {
|
|
227
|
+
await clickPoint(client, outsidePoint.x, outsidePoint.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
228
|
+
attempts.push({
|
|
229
|
+
mode: "outside-click",
|
|
230
|
+
point: outsidePoint
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
attempts.push({
|
|
234
|
+
mode: "outside-click-error",
|
|
235
|
+
point: outsidePoint,
|
|
236
|
+
error: error?.message || String(error)
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
await sleep(waitMs);
|
|
240
|
+
|
|
241
|
+
probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
242
|
+
if (!probe.open) {
|
|
243
|
+
return {
|
|
244
|
+
closed: true,
|
|
245
|
+
already_closed: false,
|
|
246
|
+
probe,
|
|
247
|
+
attempts
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const resolvedRoots = await resolveBlockingPanelRoots(client, { roots, rootState, resolveRoots });
|
|
252
|
+
const closeTarget = await findVisibleCloseTarget(client, resolvedRoots, closeSelectors);
|
|
253
|
+
if (closeTarget) {
|
|
254
|
+
try {
|
|
255
|
+
if (closeTarget.center) {
|
|
256
|
+
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
257
|
+
}
|
|
258
|
+
attempts.push({
|
|
259
|
+
mode: "close-selector",
|
|
260
|
+
selector: closeTarget.selector,
|
|
261
|
+
root: closeTarget.root
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
attempts.push({
|
|
265
|
+
mode: "close-selector-error",
|
|
266
|
+
selector: closeTarget.selector,
|
|
267
|
+
root: closeTarget.root,
|
|
268
|
+
error: error?.message || String(error)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
await sleep(waitMs);
|
|
272
|
+
|
|
273
|
+
probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
274
|
+
if (!probe.open) {
|
|
275
|
+
return {
|
|
276
|
+
closed: true,
|
|
277
|
+
already_closed: false,
|
|
278
|
+
probe,
|
|
279
|
+
attempts
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await pressEscape(client);
|
|
286
|
+
attempts.push({ mode: closeTarget ? "Escape-fallback" : "Escape" });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
attempts.push({
|
|
289
|
+
mode: "Escape-error",
|
|
290
|
+
error: error?.message || String(error)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
await sleep(waitMs);
|
|
294
|
+
|
|
295
|
+
probe = await findBossAccountRightsBlockingPanel(client, { textQueries });
|
|
296
|
+
if (!probe.open) {
|
|
297
|
+
return {
|
|
298
|
+
closed: true,
|
|
299
|
+
already_closed: false,
|
|
300
|
+
probe,
|
|
301
|
+
attempts
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
closed: false,
|
|
308
|
+
already_closed: false,
|
|
309
|
+
reason: "account_rights_panel_still_visible_after_close_attempts",
|
|
310
|
+
probe,
|
|
311
|
+
attempts
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
@@ -9,11 +9,15 @@ import {
|
|
|
9
9
|
querySelectorAll,
|
|
10
10
|
sleep
|
|
11
11
|
} from "../../core/browser/index.js";
|
|
12
|
-
import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
|
|
13
|
-
import {
|
|
14
|
-
buildScreeningCandidateFromDetail,
|
|
15
|
-
htmlToText
|
|
16
|
-
} from "../../core/screening/index.js";
|
|
12
|
+
import { candidateKeyFromProfile } from "../../core/infinite-list/index.js";
|
|
13
|
+
import {
|
|
14
|
+
buildScreeningCandidateFromDetail,
|
|
15
|
+
htmlToText
|
|
16
|
+
} from "../../core/screening/index.js";
|
|
17
|
+
import {
|
|
18
|
+
closeBossAccountRightsBlockingPanel,
|
|
19
|
+
findBossAccountRightsBlockingPanel
|
|
20
|
+
} from "../common/account-rights-panel.js";
|
|
17
21
|
import {
|
|
18
22
|
DETAIL_CLOSE_SELECTORS,
|
|
19
23
|
DETAIL_NETWORK_PATTERNS,
|
|
@@ -41,7 +45,7 @@ export function matchesRecommendDetailNetwork(url) {
|
|
|
41
45
|
return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export function createRecommendDetailNetworkRecorder(client) {
|
|
48
|
+
export function createRecommendDetailNetworkRecorder(client) {
|
|
45
49
|
const events = [];
|
|
46
50
|
client.Network.responseReceived((event) => {
|
|
47
51
|
const url = event?.response?.url || "";
|
|
@@ -76,9 +80,20 @@ export function createRecommendDetailNetworkRecorder(client) {
|
|
|
76
80
|
events.length = 0;
|
|
77
81
|
}
|
|
78
82
|
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function findRecommendBlockingPanel(client, options = {}) {
|
|
86
|
+
return findBossAccountRightsBlockingPanel(client, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function closeRecommendBlockingPanels(client, options = {}) {
|
|
90
|
+
return closeBossAccountRightsBlockingPanel(client, {
|
|
91
|
+
resolveRoots: getRecommendRoots,
|
|
92
|
+
...options
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function waitForRecommendDetailNetworkEvents(recorder, {
|
|
82
97
|
minCount = 1,
|
|
83
98
|
requireLoaded = true,
|
|
84
99
|
timeoutMs = 3500,
|
|
@@ -45,7 +45,8 @@ import {
|
|
|
45
45
|
llmResultToScreening,
|
|
46
46
|
screenCandidate
|
|
47
47
|
} from "../../core/screening/index.js";
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
closeRecommendBlockingPanels,
|
|
49
50
|
closeRecommendDetail,
|
|
50
51
|
createRecommendDetailNetworkRecorder,
|
|
51
52
|
extractRecommendDetailCandidate,
|
|
@@ -465,12 +466,17 @@ function countPassedResults(results = []) {
|
|
|
465
466
|
|
|
466
467
|
function compactCloseResult(closeResult) {
|
|
467
468
|
if (!closeResult) return null;
|
|
468
|
-
|
|
469
|
+
const result = {
|
|
469
470
|
closed: Boolean(closeResult.closed),
|
|
470
471
|
reason: closeResult.reason || null,
|
|
472
|
+
probe: closeResult.probe || null,
|
|
471
473
|
attempts: closeResult.attempts || [],
|
|
472
474
|
verification: closeResult.verification || null
|
|
473
475
|
};
|
|
476
|
+
if (closeResult.already_closed !== undefined) {
|
|
477
|
+
result.already_closed = Boolean(closeResult.already_closed);
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
474
480
|
}
|
|
475
481
|
|
|
476
482
|
function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
@@ -482,6 +488,9 @@ function compactError(error, fallbackCode = "RECOMMEND_RUN_ERROR") {
|
|
|
482
488
|
if (error.close_result) {
|
|
483
489
|
result.close_result = compactCloseResult(error.close_result);
|
|
484
490
|
}
|
|
491
|
+
if (error.phase) {
|
|
492
|
+
result.phase = error.phase;
|
|
493
|
+
}
|
|
485
494
|
if (error.refresh_attempt) {
|
|
486
495
|
result.refresh_attempt = error.refresh_attempt;
|
|
487
496
|
}
|
|
@@ -507,6 +516,14 @@ function createRecommendCloseFailureError(closeResult) {
|
|
|
507
516
|
return error;
|
|
508
517
|
}
|
|
509
518
|
|
|
519
|
+
function createRecommendBlockingPanelCloseFailureError(closeResult, phase = "") {
|
|
520
|
+
const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
|
|
521
|
+
error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
|
|
522
|
+
error.close_result = closeResult || null;
|
|
523
|
+
error.phase = phase || null;
|
|
524
|
+
return error;
|
|
525
|
+
}
|
|
526
|
+
|
|
510
527
|
function createRecommendRefreshFailureError(refreshAttempt, {
|
|
511
528
|
listEndReason = "",
|
|
512
529
|
targetCount = 0,
|
|
@@ -699,10 +716,11 @@ export async function runRecommendWorkflow({
|
|
|
699
716
|
let refreshRounds = 0;
|
|
700
717
|
let contextRecoveryAttempts = 0;
|
|
701
718
|
let greetCount = 0;
|
|
702
|
-
const candidateRecoveryCounts = new Map();
|
|
703
|
-
let jobSelection = null;
|
|
719
|
+
const candidateRecoveryCounts = new Map();
|
|
720
|
+
let jobSelection = null;
|
|
704
721
|
let pageScopeSelection = null;
|
|
705
722
|
let filterResult = null;
|
|
723
|
+
let rootState = null;
|
|
706
724
|
let cardNodeIds = [];
|
|
707
725
|
let listEndReason = "";
|
|
708
726
|
let lastHumanEvent = null;
|
|
@@ -769,9 +787,9 @@ export async function runRecommendWorkflow({
|
|
|
769
787
|
});
|
|
770
788
|
}
|
|
771
789
|
|
|
772
|
-
function checkpointInProgressCandidate({
|
|
773
|
-
index = results.length,
|
|
774
|
-
candidateKey = "",
|
|
790
|
+
function checkpointInProgressCandidate({
|
|
791
|
+
index = results.length,
|
|
792
|
+
candidateKey = "",
|
|
775
793
|
cardNodeId = null,
|
|
776
794
|
detailStep = "",
|
|
777
795
|
error = null
|
|
@@ -785,11 +803,22 @@ export async function runRecommendWorkflow({
|
|
|
785
803
|
counters: countRecommendResultStatuses(results, { greetCount }),
|
|
786
804
|
error: compactError(error, "RECOMMEND_IN_PROGRESS_ERROR")
|
|
787
805
|
},
|
|
788
|
-
candidate_list: compactInfiniteListState(listState)
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
async function
|
|
806
|
+
candidate_list: compactInfiniteListState(listState)
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function closeRecommendBlockingPanelsForRun(phase = "cleanup") {
|
|
811
|
+
const result = await closeRecommendBlockingPanels(client, {
|
|
812
|
+
attemptsLimit: 2,
|
|
813
|
+
rootState
|
|
814
|
+
});
|
|
815
|
+
if (!result?.closed) {
|
|
816
|
+
throw createRecommendBlockingPanelCloseFailureError(result, phase);
|
|
817
|
+
}
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function recoverAndReapplyRecommendContext(reason = "context_recovery", error = null, {
|
|
793
822
|
forceRecentNotView = true
|
|
794
823
|
} = {}) {
|
|
795
824
|
await runControl.waitIfPaused();
|
|
@@ -797,9 +826,9 @@ export async function runRecommendWorkflow({
|
|
|
797
826
|
const started = Date.now();
|
|
798
827
|
runControl.setPhase("recommend:recover-context");
|
|
799
828
|
contextRecoveryAttempts += 1;
|
|
800
|
-
const refreshResult = await refreshRecommendListAtEnd(client, {
|
|
801
|
-
rootState,
|
|
802
|
-
jobLabel,
|
|
829
|
+
const refreshResult = await refreshRecommendListAtEnd(client, {
|
|
830
|
+
rootState,
|
|
831
|
+
jobLabel,
|
|
803
832
|
pageScope: pageScopeSelection?.effective_scope || requestedPageScope,
|
|
804
833
|
fallbackPageScope: normalizedFallbackPageScope,
|
|
805
834
|
filter: normalizedFilter,
|
|
@@ -808,16 +837,24 @@ export async function runRecommendWorkflow({
|
|
|
808
837
|
targetUrl: targetUrl || RECOMMEND_TARGET_URL,
|
|
809
838
|
forceRecentNotView,
|
|
810
839
|
cardTimeoutMs,
|
|
811
|
-
buttonSettleMs: refreshButtonSettleMs,
|
|
812
|
-
reloadSettleMs: refreshReloadSettleMs
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
840
|
+
buttonSettleMs: refreshButtonSettleMs,
|
|
841
|
+
reloadSettleMs: refreshReloadSettleMs
|
|
842
|
+
});
|
|
843
|
+
let blockingPanelClose = null;
|
|
844
|
+
if (refreshResult.ok) {
|
|
845
|
+
blockingPanelClose = await closeRecommendBlockingPanels(client, {
|
|
846
|
+
attemptsLimit: 2,
|
|
847
|
+
rootState: refreshResult.root_state || rootState
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const compactRefresh = {
|
|
851
|
+
...compactRefreshAttempt(refreshResult),
|
|
852
|
+
context_recovery: true,
|
|
853
|
+
recovery_reason: reason,
|
|
854
|
+
trigger_error: compactError(error, "RECOMMEND_CONTEXT_RECOVERY_TRIGGER"),
|
|
855
|
+
account_rights_panel_close: compactCloseResult(blockingPanelClose),
|
|
856
|
+
elapsed_ms: Date.now() - started
|
|
857
|
+
};
|
|
821
858
|
refreshAttempts.push(compactRefresh);
|
|
822
859
|
runControl.checkpoint({
|
|
823
860
|
context_recovery: {
|
|
@@ -834,10 +871,15 @@ export async function runRecommendWorkflow({
|
|
|
834
871
|
refresh_method: refreshResult.method || null,
|
|
835
872
|
refresh_forced_recent_not_view: forceRecentNotView,
|
|
836
873
|
recovery_reason: reason
|
|
837
|
-
});
|
|
838
|
-
throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
|
|
839
|
-
}
|
|
840
|
-
|
|
874
|
+
});
|
|
875
|
+
throw new Error(`Recommend context recovery failed after ${reason}: ${refreshResult.reason || refreshResult.error || "refresh returned no cards"}`);
|
|
876
|
+
}
|
|
877
|
+
if (!blockingPanelClose?.closed) {
|
|
878
|
+
const panelError = createRecommendBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
|
|
879
|
+
panelError.refresh_attempt = compactRefresh;
|
|
880
|
+
throw panelError;
|
|
881
|
+
}
|
|
882
|
+
rootState = refreshResult.root_state || await getRecommendRoots(client);
|
|
841
883
|
rootState = await ensureRecommendViewport(rootState, "recover_after");
|
|
842
884
|
cardNodeIds = await waitForRecommendCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
843
885
|
timeoutMs: cardTimeoutMs,
|
|
@@ -863,13 +905,14 @@ export async function runRecommendWorkflow({
|
|
|
863
905
|
return refreshResult;
|
|
864
906
|
}
|
|
865
907
|
|
|
866
|
-
runControl.setPhase("recommend:cleanup");
|
|
867
|
-
await closeRecommendDetail(client, { attemptsLimit: 2 });
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
runControl.
|
|
871
|
-
runControl.
|
|
872
|
-
|
|
908
|
+
runControl.setPhase("recommend:cleanup");
|
|
909
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 });
|
|
910
|
+
await closeRecommendBlockingPanelsForRun("cleanup");
|
|
911
|
+
|
|
912
|
+
await runControl.waitIfPaused();
|
|
913
|
+
runControl.throwIfCanceled();
|
|
914
|
+
runControl.setPhase("recommend:roots");
|
|
915
|
+
rootState = await getRecommendRoots(client);
|
|
873
916
|
rootState = await ensureRecommendViewport(rootState, "roots");
|
|
874
917
|
runControl.checkpoint({
|
|
875
918
|
iframe_selector: rootState.iframe.selector,
|
|
@@ -1067,9 +1110,28 @@ export async function runRecommendWorkflow({
|
|
|
1067
1110
|
try {
|
|
1068
1111
|
await runControl.waitIfPaused();
|
|
1069
1112
|
runControl.throwIfCanceled();
|
|
1070
|
-
runControl.setPhase("recommend:detail");
|
|
1071
|
-
detailStep = "ensure_viewport";
|
|
1072
|
-
rootState = await ensureRecommendViewport(rootState, "detail");
|
|
1113
|
+
runControl.setPhase("recommend:detail");
|
|
1114
|
+
detailStep = "ensure_viewport";
|
|
1115
|
+
rootState = await ensureRecommendViewport(rootState, "detail");
|
|
1116
|
+
const blockingPanelClose = await closeRecommendBlockingPanels(client, {
|
|
1117
|
+
attemptsLimit: 2,
|
|
1118
|
+
rootState
|
|
1119
|
+
});
|
|
1120
|
+
if (!blockingPanelClose?.closed) {
|
|
1121
|
+
const panelError = createRecommendBlockingPanelCloseFailureError(
|
|
1122
|
+
blockingPanelClose,
|
|
1123
|
+
"before_detail_open"
|
|
1124
|
+
);
|
|
1125
|
+
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
1126
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
|
|
1127
|
+
await recoverAndReapplyRecommendContext("account_rights_panel_before_detail", panelError, {
|
|
1128
|
+
forceRecentNotView: true
|
|
1129
|
+
});
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if (blockingPanelClose.already_closed === false) {
|
|
1133
|
+
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
1134
|
+
}
|
|
1073
1135
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
1074
1136
|
detailStep = "open_detail";
|
|
1075
1137
|
networkRecorder.clear();
|
|
@@ -1182,12 +1244,13 @@ export async function runRecommendWorkflow({
|
|
|
1182
1244
|
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1183
1245
|
if (recoveryCount < 1) {
|
|
1184
1246
|
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1185
|
-
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
1186
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1187
|
-
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1188
|
-
await
|
|
1189
|
-
|
|
1190
|
-
|
|
1247
|
+
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
1248
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1249
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1250
|
+
await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1251
|
+
await recoverAndReapplyRecommendContext(`image_capture:${detailStep}`, error, {
|
|
1252
|
+
forceRecentNotView: true
|
|
1253
|
+
});
|
|
1191
1254
|
continue;
|
|
1192
1255
|
}
|
|
1193
1256
|
imageEvidence = createRecoverableImageCaptureEvidence(error, {
|
|
@@ -1232,20 +1295,22 @@ export async function runRecommendWorkflow({
|
|
|
1232
1295
|
if (!isRecoverableRecommendDetailError(error)) throw error;
|
|
1233
1296
|
const recoveryCount = candidateRecoveryCounts.get(candidateKey) || 0;
|
|
1234
1297
|
if (recoveryCount < 1) {
|
|
1235
|
-
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1236
|
-
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
1237
|
-
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1238
|
-
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1239
|
-
await
|
|
1240
|
-
|
|
1241
|
-
|
|
1298
|
+
candidateRecoveryCounts.set(candidateKey, recoveryCount + 1);
|
|
1299
|
+
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
1300
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
1301
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1302
|
+
await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1303
|
+
await recoverAndReapplyRecommendContext(`detail:${detailStep}`, error, {
|
|
1304
|
+
forceRecentNotView: true
|
|
1305
|
+
});
|
|
1242
1306
|
continue;
|
|
1243
1307
|
}
|
|
1244
|
-
recoverableDetailError = error;
|
|
1245
|
-
detailResult = null;
|
|
1246
|
-
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
1247
|
-
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1248
|
-
|
|
1308
|
+
recoverableDetailError = error;
|
|
1309
|
+
detailResult = null;
|
|
1310
|
+
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
1311
|
+
await closeRecommendDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
1312
|
+
await closeRecommendBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
1313
|
+
}
|
|
1249
1314
|
}
|
|
1250
1315
|
|
|
1251
1316
|
await runControl.waitIfPaused();
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
buildScreeningCandidateFromDetail,
|
|
14
14
|
htmlToText
|
|
15
15
|
} from "../../core/screening/index.js";
|
|
16
|
+
import {
|
|
17
|
+
closeBossAccountRightsBlockingPanel,
|
|
18
|
+
findBossAccountRightsBlockingPanel
|
|
19
|
+
} from "../common/account-rights-panel.js";
|
|
16
20
|
import {
|
|
17
21
|
RECRUIT_DETAIL_CLOSE_SELECTORS,
|
|
18
22
|
RECRUIT_DETAIL_NETWORK_PATTERNS,
|
|
@@ -64,6 +68,17 @@ export function createRecruitDetailNetworkRecorder(client) {
|
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
export async function findRecruitBlockingPanel(client, options = {}) {
|
|
72
|
+
return findBossAccountRightsBlockingPanel(client, options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function closeRecruitBlockingPanels(client, options = {}) {
|
|
76
|
+
return closeBossAccountRightsBlockingPanel(client, {
|
|
77
|
+
resolveRoots: getRecruitRoots,
|
|
78
|
+
...options
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
export async function waitForRecruitDetailNetworkEvents(recorder, {
|
|
68
83
|
minCount = 1,
|
|
69
84
|
requireLoaded = true,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
screenCandidate
|
|
45
45
|
} from "../../core/screening/index.js";
|
|
46
46
|
import {
|
|
47
|
+
closeRecruitBlockingPanels,
|
|
47
48
|
closeRecruitDetail,
|
|
48
49
|
createRecruitDetailNetworkRecorder,
|
|
49
50
|
extractRecruitDetailCandidate,
|
|
@@ -159,6 +160,12 @@ function compactError(error, fallbackCode = "RECRUIT_RUN_ERROR") {
|
|
|
159
160
|
code: error.code || fallbackCode,
|
|
160
161
|
message: error.message || String(error)
|
|
161
162
|
};
|
|
163
|
+
if (error.close_result) {
|
|
164
|
+
result.close_result = compactCloseResult(error.close_result);
|
|
165
|
+
}
|
|
166
|
+
if (error.phase) {
|
|
167
|
+
result.phase = error.phase;
|
|
168
|
+
}
|
|
162
169
|
if (error.refresh_attempt) {
|
|
163
170
|
result.refresh_attempt = error.refresh_attempt;
|
|
164
171
|
}
|
|
@@ -174,6 +181,21 @@ function compactError(error, fallbackCode = "RECRUIT_RUN_ERROR") {
|
|
|
174
181
|
return result;
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
function compactCloseResult(closeResult) {
|
|
185
|
+
if (!closeResult) return null;
|
|
186
|
+
const result = {
|
|
187
|
+
closed: Boolean(closeResult.closed),
|
|
188
|
+
reason: closeResult.reason || null,
|
|
189
|
+
probe: closeResult.probe || null,
|
|
190
|
+
attempts: closeResult.attempts || [],
|
|
191
|
+
verification: closeResult.verification || null
|
|
192
|
+
};
|
|
193
|
+
if (closeResult.already_closed !== undefined) {
|
|
194
|
+
result.already_closed = Boolean(closeResult.already_closed);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
function createRecruitCloseFailureError(closeResult) {
|
|
178
200
|
const error = new Error(closeResult?.reason || "Recruit detail did not close before recovery");
|
|
179
201
|
error.code = "DETAIL_CLOSE_FAILED";
|
|
@@ -181,6 +203,14 @@ function createRecruitCloseFailureError(closeResult) {
|
|
|
181
203
|
return error;
|
|
182
204
|
}
|
|
183
205
|
|
|
206
|
+
function createRecruitBlockingPanelCloseFailureError(closeResult, phase = "") {
|
|
207
|
+
const error = new Error(closeResult?.reason || "Boss account-rights panel did not close before recovery");
|
|
208
|
+
error.code = "ACCOUNT_RIGHTS_PANEL_CLOSE_FAILED";
|
|
209
|
+
error.close_result = closeResult || null;
|
|
210
|
+
error.phase = phase || null;
|
|
211
|
+
return error;
|
|
212
|
+
}
|
|
213
|
+
|
|
184
214
|
function createRecruitRefreshFailureError(refreshAttempt, {
|
|
185
215
|
listEndReason = "",
|
|
186
216
|
targetCount = 0,
|
|
@@ -397,6 +427,7 @@ export async function runRecruitWorkflow({
|
|
|
397
427
|
let refreshRounds = 0;
|
|
398
428
|
let contextRecoveryAttempts = 0;
|
|
399
429
|
const candidateRecoveryCounts = new Map();
|
|
430
|
+
let rootState = null;
|
|
400
431
|
let cardNodeIds = [];
|
|
401
432
|
let listEndReason = "";
|
|
402
433
|
let lastHumanEvent = null;
|
|
@@ -482,6 +513,17 @@ export async function runRecruitWorkflow({
|
|
|
482
513
|
});
|
|
483
514
|
}
|
|
484
515
|
|
|
516
|
+
async function closeRecruitBlockingPanelsForRun(phase = "cleanup") {
|
|
517
|
+
const result = await closeRecruitBlockingPanels(client, {
|
|
518
|
+
attemptsLimit: 2,
|
|
519
|
+
rootState
|
|
520
|
+
});
|
|
521
|
+
if (!result?.closed) {
|
|
522
|
+
throw createRecruitBlockingPanelCloseFailureError(result, phase);
|
|
523
|
+
}
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
|
|
485
527
|
async function recoverAndReapplyRecruitContext(reason = "context_recovery", error = null, {
|
|
486
528
|
forceRecentViewed = true
|
|
487
529
|
} = {}) {
|
|
@@ -499,11 +541,18 @@ export async function runRecruitWorkflow({
|
|
|
499
541
|
cityOptionTimeoutMs,
|
|
500
542
|
forceRecentViewed
|
|
501
543
|
});
|
|
544
|
+
let blockingPanelClose = null;
|
|
545
|
+
if (refreshResult.ok) {
|
|
546
|
+
blockingPanelClose = await closeRecruitBlockingPanels(client, {
|
|
547
|
+
attemptsLimit: 2
|
|
548
|
+
});
|
|
549
|
+
}
|
|
502
550
|
const compactRefresh = {
|
|
503
551
|
...compactRefreshAttempt(refreshResult),
|
|
504
552
|
context_recovery: true,
|
|
505
553
|
recovery_reason: reason,
|
|
506
554
|
trigger_error: compactError(error, "RECRUIT_CONTEXT_RECOVERY_TRIGGER"),
|
|
555
|
+
account_rights_panel_close: compactCloseResult(blockingPanelClose),
|
|
507
556
|
elapsed_ms: Date.now() - started
|
|
508
557
|
};
|
|
509
558
|
refreshAttempts.push(compactRefresh);
|
|
@@ -525,6 +574,11 @@ export async function runRecruitWorkflow({
|
|
|
525
574
|
});
|
|
526
575
|
throw new Error(`Recruit context recovery failed after ${reason}: ${refreshResult.application?.reason || "refresh returned no cards"}`);
|
|
527
576
|
}
|
|
577
|
+
if (!blockingPanelClose?.closed) {
|
|
578
|
+
const panelError = createRecruitBlockingPanelCloseFailureError(blockingPanelClose, `recover:${reason}`);
|
|
579
|
+
panelError.refresh_attempt = compactRefresh;
|
|
580
|
+
throw panelError;
|
|
581
|
+
}
|
|
528
582
|
rootState = await getRecruitRoots(client);
|
|
529
583
|
rootState = await ensureRecruitViewport(rootState, "recover_after");
|
|
530
584
|
cardNodeIds = await waitForRecruitCardNodeIds(client, rootState.iframe.documentNodeId, {
|
|
@@ -553,11 +607,12 @@ export async function runRecruitWorkflow({
|
|
|
553
607
|
|
|
554
608
|
runControl.setPhase("recruit:cleanup");
|
|
555
609
|
await closeRecruitDetail(client, { attemptsLimit: 2 });
|
|
610
|
+
await closeRecruitBlockingPanelsForRun("cleanup");
|
|
556
611
|
|
|
557
612
|
await runControl.waitIfPaused();
|
|
558
613
|
runControl.throwIfCanceled();
|
|
559
614
|
runControl.setPhase("recruit:roots");
|
|
560
|
-
|
|
615
|
+
rootState = await getRecruitRoots(client);
|
|
561
616
|
rootState = await ensureRecruitViewport(rootState, "roots");
|
|
562
617
|
runControl.checkpoint({
|
|
563
618
|
iframe_selector: rootState.iframe.selector,
|
|
@@ -732,6 +787,25 @@ export async function runRecruitWorkflow({
|
|
|
732
787
|
runControl.setPhase("recruit:detail");
|
|
733
788
|
detailStep = "ensure_viewport";
|
|
734
789
|
rootState = await ensureRecruitViewport(rootState, "detail");
|
|
790
|
+
const blockingPanelClose = await closeRecruitBlockingPanels(client, {
|
|
791
|
+
attemptsLimit: 2,
|
|
792
|
+
rootState
|
|
793
|
+
});
|
|
794
|
+
if (!blockingPanelClose?.closed) {
|
|
795
|
+
const panelError = createRecruitBlockingPanelCloseFailureError(
|
|
796
|
+
blockingPanelClose,
|
|
797
|
+
"before_detail_open"
|
|
798
|
+
);
|
|
799
|
+
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
800
|
+
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error: panelError });
|
|
801
|
+
await recoverAndReapplyRecruitContext("account_rights_panel_before_detail", panelError, {
|
|
802
|
+
forceRecentViewed: true
|
|
803
|
+
});
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (blockingPanelClose.already_closed === false) {
|
|
807
|
+
timings.account_rights_panel_close = compactCloseResult(blockingPanelClose);
|
|
808
|
+
}
|
|
735
809
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep });
|
|
736
810
|
detailStep = "open_detail";
|
|
737
811
|
networkRecorder.clear();
|
|
@@ -835,6 +909,7 @@ export async function runRecruitWorkflow({
|
|
|
835
909
|
timings.image_capture_recovery_trigger = compactError(error, "IMAGE_CAPTURE_FAILED");
|
|
836
910
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
837
911
|
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
912
|
+
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
838
913
|
await recoverAndReapplyRecruitContext(`image_capture:${detailStep}`, error, {
|
|
839
914
|
forceRecentViewed: true
|
|
840
915
|
});
|
|
@@ -907,6 +982,7 @@ export async function runRecruitWorkflow({
|
|
|
907
982
|
timings.detail_recovery_trigger = compactRecoverableDetailError(error);
|
|
908
983
|
checkpointInProgressCandidate({ index, candidateKey, cardNodeId, detailStep, error });
|
|
909
984
|
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
985
|
+
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
910
986
|
await recoverAndReapplyRecruitContext(`detail:${detailStep}`, error, {
|
|
911
987
|
forceRecentViewed: true
|
|
912
988
|
});
|
|
@@ -916,6 +992,7 @@ export async function runRecruitWorkflow({
|
|
|
916
992
|
detailResult = null;
|
|
917
993
|
timings.detail_recovered_error = compactRecoverableDetailError(error);
|
|
918
994
|
await closeRecruitDetail(client, { attemptsLimit: 2 }).catch(() => null);
|
|
995
|
+
await closeRecruitBlockingPanels(client, { attemptsLimit: 2, rootState }).catch(() => null);
|
|
919
996
|
}
|
|
920
997
|
}
|
|
921
998
|
|