@reconcrap/boss-recommend-mcp 2.0.53 → 2.0.54
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/bin/boss-recommend-mcp.js +0 -0
- package/config/screening-config.example.json +1 -1
- package/package.json +120 -120
- package/src/cli.js +3121 -3121
- package/src/core/run/index.js +310 -310
- package/src/domains/chat/constants.js +245 -221
- package/src/domains/chat/detail.js +1853 -1668
- package/src/domains/chat/run-service.js +2039 -1979
- package/src/domains/recommend/detail.js +544 -544
- package/src/domains/recommend/run-service.js +1235 -1235
- package/src/recommend-mcp.js +1701 -1701
- package/src/run-state.js +358 -358
|
@@ -3,26 +3,26 @@ import {
|
|
|
3
3
|
clickPoint,
|
|
4
4
|
DETERMINISTIC_CLICK_OPTIONS,
|
|
5
5
|
getFrameDocumentNodeId,
|
|
6
|
-
getNodeBox,
|
|
7
|
-
getOuterHTML,
|
|
8
|
-
pressKey,
|
|
9
|
-
querySelectorAll,
|
|
10
|
-
sleep
|
|
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";
|
|
17
|
-
import {
|
|
18
|
-
DETAIL_CLOSE_SELECTORS,
|
|
19
|
-
DETAIL_NETWORK_PATTERNS,
|
|
20
|
-
DETAIL_POPUP_SELECTORS,
|
|
21
|
-
DETAIL_RESUME_IFRAME_SELECTORS
|
|
22
|
-
} from "./constants.js";
|
|
23
|
-
import {
|
|
24
|
-
getRecommendRoots
|
|
25
|
-
} from "./roots.js";
|
|
6
|
+
getNodeBox,
|
|
7
|
+
getOuterHTML,
|
|
8
|
+
pressKey,
|
|
9
|
+
querySelectorAll,
|
|
10
|
+
sleep
|
|
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";
|
|
17
|
+
import {
|
|
18
|
+
DETAIL_CLOSE_SELECTORS,
|
|
19
|
+
DETAIL_NETWORK_PATTERNS,
|
|
20
|
+
DETAIL_POPUP_SELECTORS,
|
|
21
|
+
DETAIL_RESUME_IFRAME_SELECTORS
|
|
22
|
+
} from "./constants.js";
|
|
23
|
+
import {
|
|
24
|
+
getRecommendRoots
|
|
25
|
+
} from "./roots.js";
|
|
26
26
|
import {
|
|
27
27
|
findRecommendCardNodeIds,
|
|
28
28
|
readRecommendCardCandidate
|
|
@@ -38,149 +38,149 @@ const DETAIL_OUTSIDE_CLOSE_BOUNDARY_SELECTORS = Object.freeze([
|
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
40
|
export function matchesRecommendDetailNetwork(url) {
|
|
41
|
-
return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function createRecommendDetailNetworkRecorder(client) {
|
|
45
|
-
const events = [];
|
|
46
|
-
client.Network.responseReceived((event) => {
|
|
47
|
-
const url = event?.response?.url || "";
|
|
48
|
-
if (!matchesRecommendDetailNetwork(url)) return;
|
|
49
|
-
events.push({
|
|
50
|
-
requestId: event.requestId,
|
|
51
|
-
url,
|
|
52
|
-
status: event.response?.status,
|
|
53
|
-
mimeType: event.response?.mimeType,
|
|
54
|
-
type: event.type
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
if (typeof client.Network.loadingFinished === "function") {
|
|
58
|
-
client.Network.loadingFinished((event) => {
|
|
59
|
-
const found = events.find((item) => item.requestId === event.requestId);
|
|
60
|
-
if (!found) return;
|
|
61
|
-
found.loading_finished = true;
|
|
62
|
-
found.encodedDataLength = event.encodedDataLength;
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
if (typeof client.Network.loadingFailed === "function") {
|
|
66
|
-
client.Network.loadingFailed((event) => {
|
|
67
|
-
const found = events.find((item) => item.requestId === event.requestId);
|
|
68
|
-
if (!found) return;
|
|
69
|
-
found.loading_failed = true;
|
|
70
|
-
found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
events,
|
|
75
|
-
clear() {
|
|
76
|
-
events.length = 0;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function waitForRecommendDetailNetworkEvents(recorder, {
|
|
82
|
-
minCount = 1,
|
|
83
|
-
requireLoaded = true,
|
|
84
|
-
timeoutMs = 3500,
|
|
85
|
-
intervalMs = 100
|
|
86
|
-
} = {}) {
|
|
87
|
-
const started = Date.now();
|
|
88
|
-
const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
|
|
89
|
-
let matching = [];
|
|
90
|
-
while (Date.now() - started <= timeoutMs) {
|
|
91
|
-
matching = events.filter((event) => (
|
|
92
|
-
!requireLoaded
|
|
93
|
-
|| event.loading_finished === true
|
|
94
|
-
|| event.loading_failed === true
|
|
95
|
-
));
|
|
96
|
-
if (matching.length >= minCount) {
|
|
97
|
-
return {
|
|
98
|
-
ok: true,
|
|
99
|
-
elapsed_ms: Date.now() - started,
|
|
100
|
-
count: matching.length,
|
|
101
|
-
events: matching
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
await sleep(intervalMs);
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
ok: false,
|
|
108
|
-
elapsed_ms: Date.now() - started,
|
|
109
|
-
count: matching.length,
|
|
110
|
-
events: matching,
|
|
111
|
-
total_event_count: events.length
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function readRecommendDetailNetworkBodies(client, events = [], {
|
|
116
|
-
limit = 10
|
|
117
|
-
} = {}) {
|
|
118
|
-
const bodies = [];
|
|
119
|
-
for (const event of events.slice(0, limit)) {
|
|
120
|
-
try {
|
|
121
|
-
const body = await client.Network.getResponseBody({ requestId: event.requestId });
|
|
122
|
-
bodies.push({
|
|
123
|
-
...event,
|
|
124
|
-
body,
|
|
125
|
-
body_length: String(body?.body || "").length
|
|
126
|
-
});
|
|
127
|
-
} catch (error) {
|
|
128
|
-
bodies.push({
|
|
129
|
-
...event,
|
|
130
|
-
body_error: error?.message || String(error)
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return bodies;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export async function waitForRecommendDetail(client, {
|
|
138
|
-
timeoutMs = 10000,
|
|
139
|
-
intervalMs = 250
|
|
140
|
-
} = {}) {
|
|
141
|
-
const started = Date.now();
|
|
142
|
-
let lastState = null;
|
|
143
|
-
while (Date.now() - started <= timeoutMs) {
|
|
144
|
-
lastState = await readRecommendDetailState(client);
|
|
145
|
-
if (lastState?.popup || lastState?.resumeIframe) return lastState;
|
|
146
|
-
await sleep(intervalMs);
|
|
147
|
-
}
|
|
148
|
-
return lastState;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function readRecommendDetailState(client) {
|
|
152
|
-
const rootState = await getRecommendRoots(client);
|
|
153
|
-
const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
|
|
154
|
-
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
|
|
155
|
-
return {
|
|
156
|
-
iframe: rootState.iframe,
|
|
157
|
-
roots: rootState.roots,
|
|
158
|
-
popup,
|
|
159
|
-
resumeIframe
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
41
|
+
return DETAIL_NETWORK_PATTERNS.some((pattern) => pattern.test(String(url || "")));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createRecommendDetailNetworkRecorder(client) {
|
|
45
|
+
const events = [];
|
|
46
|
+
client.Network.responseReceived((event) => {
|
|
47
|
+
const url = event?.response?.url || "";
|
|
48
|
+
if (!matchesRecommendDetailNetwork(url)) return;
|
|
49
|
+
events.push({
|
|
50
|
+
requestId: event.requestId,
|
|
51
|
+
url,
|
|
52
|
+
status: event.response?.status,
|
|
53
|
+
mimeType: event.response?.mimeType,
|
|
54
|
+
type: event.type
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
if (typeof client.Network.loadingFinished === "function") {
|
|
58
|
+
client.Network.loadingFinished((event) => {
|
|
59
|
+
const found = events.find((item) => item.requestId === event.requestId);
|
|
60
|
+
if (!found) return;
|
|
61
|
+
found.loading_finished = true;
|
|
62
|
+
found.encodedDataLength = event.encodedDataLength;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (typeof client.Network.loadingFailed === "function") {
|
|
66
|
+
client.Network.loadingFailed((event) => {
|
|
67
|
+
const found = events.find((item) => item.requestId === event.requestId);
|
|
68
|
+
if (!found) return;
|
|
69
|
+
found.loading_failed = true;
|
|
70
|
+
found.loading_error = event.errorText || event.blockedReason || "Network loading failed";
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
events,
|
|
75
|
+
clear() {
|
|
76
|
+
events.length = 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function waitForRecommendDetailNetworkEvents(recorder, {
|
|
82
|
+
minCount = 1,
|
|
83
|
+
requireLoaded = true,
|
|
84
|
+
timeoutMs = 3500,
|
|
85
|
+
intervalMs = 100
|
|
86
|
+
} = {}) {
|
|
87
|
+
const started = Date.now();
|
|
88
|
+
const events = Array.isArray(recorder) ? recorder : recorder?.events || [];
|
|
89
|
+
let matching = [];
|
|
90
|
+
while (Date.now() - started <= timeoutMs) {
|
|
91
|
+
matching = events.filter((event) => (
|
|
92
|
+
!requireLoaded
|
|
93
|
+
|| event.loading_finished === true
|
|
94
|
+
|| event.loading_failed === true
|
|
95
|
+
));
|
|
96
|
+
if (matching.length >= minCount) {
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
elapsed_ms: Date.now() - started,
|
|
100
|
+
count: matching.length,
|
|
101
|
+
events: matching
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
await sleep(intervalMs);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
elapsed_ms: Date.now() - started,
|
|
109
|
+
count: matching.length,
|
|
110
|
+
events: matching,
|
|
111
|
+
total_event_count: events.length
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function readRecommendDetailNetworkBodies(client, events = [], {
|
|
116
|
+
limit = 10
|
|
117
|
+
} = {}) {
|
|
118
|
+
const bodies = [];
|
|
119
|
+
for (const event of events.slice(0, limit)) {
|
|
120
|
+
try {
|
|
121
|
+
const body = await client.Network.getResponseBody({ requestId: event.requestId });
|
|
122
|
+
bodies.push({
|
|
123
|
+
...event,
|
|
124
|
+
body,
|
|
125
|
+
body_length: String(body?.body || "").length
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
bodies.push({
|
|
129
|
+
...event,
|
|
130
|
+
body_error: error?.message || String(error)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return bodies;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function waitForRecommendDetail(client, {
|
|
138
|
+
timeoutMs = 10000,
|
|
139
|
+
intervalMs = 250
|
|
140
|
+
} = {}) {
|
|
141
|
+
const started = Date.now();
|
|
142
|
+
let lastState = null;
|
|
143
|
+
while (Date.now() - started <= timeoutMs) {
|
|
144
|
+
lastState = await readRecommendDetailState(client);
|
|
145
|
+
if (lastState?.popup || lastState?.resumeIframe) return lastState;
|
|
146
|
+
await sleep(intervalMs);
|
|
147
|
+
}
|
|
148
|
+
return lastState;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readRecommendDetailState(client) {
|
|
152
|
+
const rootState = await getRecommendRoots(client);
|
|
153
|
+
const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
|
|
154
|
+
const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
|
|
155
|
+
return {
|
|
156
|
+
iframe: rootState.iframe,
|
|
157
|
+
roots: rootState.roots,
|
|
158
|
+
popup,
|
|
159
|
+
resumeIframe
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
163
|
export async function waitForRecommendDetailClosed(client, {
|
|
164
164
|
timeoutMs = 4000,
|
|
165
165
|
intervalMs = 250
|
|
166
166
|
} = {}) {
|
|
167
|
-
const started = Date.now();
|
|
168
|
-
let lastState = null;
|
|
169
|
-
while (Date.now() - started <= timeoutMs) {
|
|
170
|
-
lastState = await readRecommendDetailState(client);
|
|
171
|
-
if (!lastState?.popup && !lastState?.resumeIframe) {
|
|
172
|
-
return {
|
|
173
|
-
closed: true,
|
|
174
|
-
elapsed_ms: Date.now() - started,
|
|
175
|
-
state: lastState
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
await sleep(intervalMs);
|
|
179
|
-
}
|
|
180
|
-
return {
|
|
181
|
-
closed: false,
|
|
182
|
-
elapsed_ms: Date.now() - started,
|
|
183
|
-
state: lastState
|
|
167
|
+
const started = Date.now();
|
|
168
|
+
let lastState = null;
|
|
169
|
+
while (Date.now() - started <= timeoutMs) {
|
|
170
|
+
lastState = await readRecommendDetailState(client);
|
|
171
|
+
if (!lastState?.popup && !lastState?.resumeIframe) {
|
|
172
|
+
return {
|
|
173
|
+
closed: true,
|
|
174
|
+
elapsed_ms: Date.now() - started,
|
|
175
|
+
state: lastState
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
await sleep(intervalMs);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
closed: false,
|
|
182
|
+
elapsed_ms: Date.now() - started,
|
|
183
|
+
state: lastState
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -242,74 +242,74 @@ async function verifyRecommendDetailStillOpen(client, {
|
|
|
242
242
|
async function findVisibleDetailTarget(client, roots, selectors) {
|
|
243
243
|
for (const root of roots) {
|
|
244
244
|
if (!root?.nodeId) continue;
|
|
245
|
-
for (const selector of selectors) {
|
|
246
|
-
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
247
|
-
for (const nodeId of nodeIds) {
|
|
248
|
-
try {
|
|
249
|
-
const box = await getNodeBox(client, nodeId);
|
|
250
|
-
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
251
|
-
return {
|
|
252
|
-
root: root.name,
|
|
253
|
-
root_node_id: root.nodeId,
|
|
254
|
-
selector,
|
|
255
|
-
node_id: nodeId,
|
|
256
|
-
center: box.center,
|
|
257
|
-
rect: box.rect
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
} catch {}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export async function readRecommendDetailHtml(client, detailState) {
|
|
268
|
-
let popupHTML = "";
|
|
269
|
-
let resumeHTML = "";
|
|
270
|
-
let resumeIframeDocumentNodeId = null;
|
|
271
|
-
const errors = [];
|
|
272
|
-
|
|
273
|
-
if (detailState?.popup?.node_id) {
|
|
274
|
-
try {
|
|
275
|
-
popupHTML = await getOuterHTML(client, detailState.popup.node_id);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
errors.push({
|
|
278
|
-
source: "popup",
|
|
279
|
-
node_id: detailState.popup.node_id,
|
|
280
|
-
stale_node: isStaleRecommendNodeError(error),
|
|
281
|
-
error: error?.message || String(error)
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (detailState?.resumeIframe?.node_id) {
|
|
287
|
-
try {
|
|
288
|
-
resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
|
|
289
|
-
resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
errors.push({
|
|
292
|
-
source: "resume_iframe",
|
|
293
|
-
node_id: detailState.resumeIframe.node_id,
|
|
294
|
-
document_node_id: resumeIframeDocumentNodeId,
|
|
295
|
-
stale_node: isStaleRecommendNodeError(error),
|
|
296
|
-
error: error?.message || String(error)
|
|
297
|
-
});
|
|
298
|
-
resumeIframeDocumentNodeId = null;
|
|
299
|
-
resumeHTML = "";
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
popupHTML,
|
|
305
|
-
resumeHTML,
|
|
306
|
-
resumeIframeDocumentNodeId,
|
|
307
|
-
popupText: htmlToText(popupHTML),
|
|
308
|
-
resumeText: htmlToText(resumeHTML),
|
|
309
|
-
errors
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
245
|
+
for (const selector of selectors) {
|
|
246
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
247
|
+
for (const nodeId of nodeIds) {
|
|
248
|
+
try {
|
|
249
|
+
const box = await getNodeBox(client, nodeId);
|
|
250
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
251
|
+
return {
|
|
252
|
+
root: root.name,
|
|
253
|
+
root_node_id: root.nodeId,
|
|
254
|
+
selector,
|
|
255
|
+
node_id: nodeId,
|
|
256
|
+
center: box.center,
|
|
257
|
+
rect: box.rect
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function readRecommendDetailHtml(client, detailState) {
|
|
268
|
+
let popupHTML = "";
|
|
269
|
+
let resumeHTML = "";
|
|
270
|
+
let resumeIframeDocumentNodeId = null;
|
|
271
|
+
const errors = [];
|
|
272
|
+
|
|
273
|
+
if (detailState?.popup?.node_id) {
|
|
274
|
+
try {
|
|
275
|
+
popupHTML = await getOuterHTML(client, detailState.popup.node_id);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
errors.push({
|
|
278
|
+
source: "popup",
|
|
279
|
+
node_id: detailState.popup.node_id,
|
|
280
|
+
stale_node: isStaleRecommendNodeError(error),
|
|
281
|
+
error: error?.message || String(error)
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (detailState?.resumeIframe?.node_id) {
|
|
287
|
+
try {
|
|
288
|
+
resumeIframeDocumentNodeId = await getFrameDocumentNodeId(client, detailState.resumeIframe.node_id);
|
|
289
|
+
resumeHTML = await getOuterHTML(client, resumeIframeDocumentNodeId);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
errors.push({
|
|
292
|
+
source: "resume_iframe",
|
|
293
|
+
node_id: detailState.resumeIframe.node_id,
|
|
294
|
+
document_node_id: resumeIframeDocumentNodeId,
|
|
295
|
+
stale_node: isStaleRecommendNodeError(error),
|
|
296
|
+
error: error?.message || String(error)
|
|
297
|
+
});
|
|
298
|
+
resumeIframeDocumentNodeId = null;
|
|
299
|
+
resumeHTML = "";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
popupHTML,
|
|
305
|
+
resumeHTML,
|
|
306
|
+
resumeIframeDocumentNodeId,
|
|
307
|
+
popupText: htmlToText(popupHTML),
|
|
308
|
+
resumeText: htmlToText(resumeHTML),
|
|
309
|
+
errors
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
313
|
export function isStaleRecommendNodeError(error) {
|
|
314
314
|
const message = String(error?.message || error || "");
|
|
315
315
|
return /Could not find node with given id|No node with given id|Node is detached|Cannot find node|Could not compute box model/i.test(message);
|
|
@@ -321,138 +321,138 @@ export function isRecommendDetailOpenMissError(error) {
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
export async function findRecommendCardNodeForCandidateKey(client, {
|
|
324
|
-
candidateKey = "",
|
|
325
|
-
rootState = null,
|
|
326
|
-
targetUrl = "",
|
|
327
|
-
source = "recommend-run-card-retry",
|
|
328
|
-
timeoutMs = 5000,
|
|
329
|
-
intervalMs = 250
|
|
330
|
-
} = {}) {
|
|
331
|
-
if (!candidateKey) {
|
|
332
|
-
return {
|
|
333
|
-
ok: false,
|
|
334
|
-
reason: "candidate_key_required"
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const started = Date.now();
|
|
339
|
-
let lastError = null;
|
|
340
|
-
let lastCardCount = 0;
|
|
341
|
-
while (Date.now() - started <= timeoutMs) {
|
|
342
|
-
const currentRootState = rootState?.iframe?.documentNodeId
|
|
343
|
-
? rootState
|
|
344
|
-
: await getRecommendRoots(client);
|
|
345
|
-
const frameNodeId = currentRootState?.iframe?.documentNodeId;
|
|
346
|
-
if (!frameNodeId) {
|
|
347
|
-
return {
|
|
348
|
-
ok: false,
|
|
349
|
-
reason: "recommend_frame_not_found"
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
|
|
354
|
-
lastCardCount = nodeIds.length;
|
|
355
|
-
for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
|
|
356
|
-
const nodeId = nodeIds[visibleIndex];
|
|
357
|
-
try {
|
|
358
|
-
const candidate = await readRecommendCardCandidate(client, nodeId, {
|
|
359
|
-
targetUrl,
|
|
360
|
-
source,
|
|
361
|
-
metadata: {
|
|
362
|
-
visible_index: visibleIndex,
|
|
363
|
-
retry_reason: "stale_detail_node"
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
const key = candidateKeyFromProfile(candidate, {
|
|
367
|
-
nodeId,
|
|
368
|
-
visibleIndex,
|
|
369
|
-
attributes: candidate?.attributes || candidate?.metadata?.attributes || {}
|
|
370
|
-
});
|
|
371
|
-
if (key === candidateKey) {
|
|
372
|
-
return {
|
|
373
|
-
ok: true,
|
|
374
|
-
node_id: nodeId,
|
|
375
|
-
visible_index: visibleIndex,
|
|
376
|
-
candidate,
|
|
377
|
-
key,
|
|
378
|
-
root_state: currentRootState,
|
|
379
|
-
card_count: nodeIds.length
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
} catch (error) {
|
|
383
|
-
lastError = error;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (intervalMs > 0) await sleep(intervalMs);
|
|
388
|
-
rootState = null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
ok: false,
|
|
393
|
-
reason: "candidate_key_not_mounted",
|
|
394
|
-
candidate_key: candidateKey,
|
|
395
|
-
last_card_count: lastCardCount,
|
|
396
|
-
error: lastError?.message || null
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export async function openRecommendCardDetail(client, cardNodeId, {
|
|
401
|
-
timeoutMs = 12000,
|
|
402
|
-
scrollIntoView = true
|
|
403
|
-
} = {}) {
|
|
404
|
-
const started = Date.now();
|
|
405
|
-
const clickStarted = Date.now();
|
|
406
|
-
const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
|
|
407
|
-
const candidateClickMs = Date.now() - clickStarted;
|
|
408
|
-
const detailStarted = Date.now();
|
|
409
|
-
const detailState = await waitForRecommendDetail(client, { timeoutMs });
|
|
410
|
-
const detailOpenMs = Date.now() - detailStarted;
|
|
411
|
-
if (!detailState?.popup && !detailState?.resumeIframe) {
|
|
412
|
-
throw new Error("Candidate detail did not open or no known detail selectors mounted");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
card_box: cardBox,
|
|
417
|
-
detail_state: detailState,
|
|
418
|
-
timings: {
|
|
419
|
-
candidate_click_ms: candidateClickMs,
|
|
420
|
-
detail_open_ms: detailOpenMs,
|
|
421
|
-
open_total_ms: Date.now() - started
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
427
|
-
cardNodeId,
|
|
428
|
-
candidateKey = "",
|
|
429
|
-
cardCandidate = null,
|
|
430
|
-
rootState = null,
|
|
431
|
-
targetUrl = "",
|
|
432
|
-
timeoutMs = 12000,
|
|
433
|
-
scrollIntoView = true,
|
|
434
|
-
retryTimeoutMs = 5000,
|
|
435
|
-
retryIntervalMs = 250,
|
|
436
|
-
maxAttempts = 2
|
|
437
|
-
} = {}) {
|
|
438
|
-
let currentNodeId = cardNodeId;
|
|
439
|
-
let currentCandidate = cardCandidate;
|
|
440
|
-
let currentRootState = rootState;
|
|
441
|
-
const attempts = [];
|
|
442
|
-
const limit = Math.max(1, Number(maxAttempts) || 1);
|
|
443
|
-
|
|
444
|
-
for (let attemptIndex = 0; attemptIndex < limit; attemptIndex += 1) {
|
|
445
|
-
try {
|
|
446
|
-
const opened = await openRecommendCardDetail(client, currentNodeId, {
|
|
447
|
-
timeoutMs,
|
|
448
|
-
scrollIntoView
|
|
449
|
-
});
|
|
450
|
-
return {
|
|
451
|
-
...opened,
|
|
452
|
-
card_node_id: currentNodeId,
|
|
453
|
-
card_candidate: currentCandidate,
|
|
454
|
-
retry_attempts: attempts
|
|
455
|
-
};
|
|
324
|
+
candidateKey = "",
|
|
325
|
+
rootState = null,
|
|
326
|
+
targetUrl = "",
|
|
327
|
+
source = "recommend-run-card-retry",
|
|
328
|
+
timeoutMs = 5000,
|
|
329
|
+
intervalMs = 250
|
|
330
|
+
} = {}) {
|
|
331
|
+
if (!candidateKey) {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
reason: "candidate_key_required"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const started = Date.now();
|
|
339
|
+
let lastError = null;
|
|
340
|
+
let lastCardCount = 0;
|
|
341
|
+
while (Date.now() - started <= timeoutMs) {
|
|
342
|
+
const currentRootState = rootState?.iframe?.documentNodeId
|
|
343
|
+
? rootState
|
|
344
|
+
: await getRecommendRoots(client);
|
|
345
|
+
const frameNodeId = currentRootState?.iframe?.documentNodeId;
|
|
346
|
+
if (!frameNodeId) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
reason: "recommend_frame_not_found"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const nodeIds = await findRecommendCardNodeIds(client, frameNodeId);
|
|
354
|
+
lastCardCount = nodeIds.length;
|
|
355
|
+
for (let visibleIndex = 0; visibleIndex < nodeIds.length; visibleIndex += 1) {
|
|
356
|
+
const nodeId = nodeIds[visibleIndex];
|
|
357
|
+
try {
|
|
358
|
+
const candidate = await readRecommendCardCandidate(client, nodeId, {
|
|
359
|
+
targetUrl,
|
|
360
|
+
source,
|
|
361
|
+
metadata: {
|
|
362
|
+
visible_index: visibleIndex,
|
|
363
|
+
retry_reason: "stale_detail_node"
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
const key = candidateKeyFromProfile(candidate, {
|
|
367
|
+
nodeId,
|
|
368
|
+
visibleIndex,
|
|
369
|
+
attributes: candidate?.attributes || candidate?.metadata?.attributes || {}
|
|
370
|
+
});
|
|
371
|
+
if (key === candidateKey) {
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
node_id: nodeId,
|
|
375
|
+
visible_index: visibleIndex,
|
|
376
|
+
candidate,
|
|
377
|
+
key,
|
|
378
|
+
root_state: currentRootState,
|
|
379
|
+
card_count: nodeIds.length
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
lastError = error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (intervalMs > 0) await sleep(intervalMs);
|
|
388
|
+
rootState = null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
reason: "candidate_key_not_mounted",
|
|
394
|
+
candidate_key: candidateKey,
|
|
395
|
+
last_card_count: lastCardCount,
|
|
396
|
+
error: lastError?.message || null
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function openRecommendCardDetail(client, cardNodeId, {
|
|
401
|
+
timeoutMs = 12000,
|
|
402
|
+
scrollIntoView = true
|
|
403
|
+
} = {}) {
|
|
404
|
+
const started = Date.now();
|
|
405
|
+
const clickStarted = Date.now();
|
|
406
|
+
const cardBox = await clickNodeCenter(client, cardNodeId, { scrollIntoView });
|
|
407
|
+
const candidateClickMs = Date.now() - clickStarted;
|
|
408
|
+
const detailStarted = Date.now();
|
|
409
|
+
const detailState = await waitForRecommendDetail(client, { timeoutMs });
|
|
410
|
+
const detailOpenMs = Date.now() - detailStarted;
|
|
411
|
+
if (!detailState?.popup && !detailState?.resumeIframe) {
|
|
412
|
+
throw new Error("Candidate detail did not open or no known detail selectors mounted");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
card_box: cardBox,
|
|
417
|
+
detail_state: detailState,
|
|
418
|
+
timings: {
|
|
419
|
+
candidate_click_ms: candidateClickMs,
|
|
420
|
+
detail_open_ms: detailOpenMs,
|
|
421
|
+
open_total_ms: Date.now() - started
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
427
|
+
cardNodeId,
|
|
428
|
+
candidateKey = "",
|
|
429
|
+
cardCandidate = null,
|
|
430
|
+
rootState = null,
|
|
431
|
+
targetUrl = "",
|
|
432
|
+
timeoutMs = 12000,
|
|
433
|
+
scrollIntoView = true,
|
|
434
|
+
retryTimeoutMs = 5000,
|
|
435
|
+
retryIntervalMs = 250,
|
|
436
|
+
maxAttempts = 2
|
|
437
|
+
} = {}) {
|
|
438
|
+
let currentNodeId = cardNodeId;
|
|
439
|
+
let currentCandidate = cardCandidate;
|
|
440
|
+
let currentRootState = rootState;
|
|
441
|
+
const attempts = [];
|
|
442
|
+
const limit = Math.max(1, Number(maxAttempts) || 1);
|
|
443
|
+
|
|
444
|
+
for (let attemptIndex = 0; attemptIndex < limit; attemptIndex += 1) {
|
|
445
|
+
try {
|
|
446
|
+
const opened = await openRecommendCardDetail(client, currentNodeId, {
|
|
447
|
+
timeoutMs,
|
|
448
|
+
scrollIntoView
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
...opened,
|
|
452
|
+
card_node_id: currentNodeId,
|
|
453
|
+
card_candidate: currentCandidate,
|
|
454
|
+
retry_attempts: attempts
|
|
455
|
+
};
|
|
456
456
|
} catch (error) {
|
|
457
457
|
const stale = isStaleRecommendNodeError(error);
|
|
458
458
|
const detailOpenMiss = isRecommendDetailOpenMissError(error);
|
|
@@ -467,88 +467,88 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
|
|
|
467
467
|
error.recommend_detail_open_attempts = attempts;
|
|
468
468
|
throw error;
|
|
469
469
|
}
|
|
470
|
-
|
|
471
|
-
const resolved = await findRecommendCardNodeForCandidateKey(client, {
|
|
472
|
-
candidateKey,
|
|
473
|
-
rootState: currentRootState,
|
|
474
|
-
targetUrl,
|
|
475
|
-
timeoutMs: retryTimeoutMs,
|
|
476
|
-
intervalMs: retryIntervalMs
|
|
477
|
-
});
|
|
478
|
-
attempts[attempts.length - 1].refresh_lookup = {
|
|
479
|
-
ok: Boolean(resolved.ok),
|
|
480
|
-
node_id: resolved.node_id || null,
|
|
481
|
-
visible_index: resolved.visible_index ?? null,
|
|
482
|
-
card_count: resolved.card_count || resolved.last_card_count || 0,
|
|
483
|
-
reason: resolved.reason || null,
|
|
484
|
-
error: resolved.error || null
|
|
485
|
-
};
|
|
486
|
-
if (!resolved.ok || !resolved.node_id) {
|
|
487
|
-
error.recommend_detail_open_attempts = attempts;
|
|
488
|
-
throw error;
|
|
489
|
-
}
|
|
490
|
-
currentNodeId = resolved.node_id;
|
|
491
|
-
currentCandidate = resolved.candidate || currentCandidate;
|
|
492
|
-
currentRootState = resolved.root_state || null;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
throw new Error("Recommend detail retry exhausted");
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
export async function closeRecommendDetail(client, {
|
|
500
|
-
attemptsLimit = 4,
|
|
501
|
-
closeWaitMs = 5000,
|
|
502
|
-
escapeWaitMs = 3500
|
|
503
|
-
} = {}) {
|
|
504
|
-
const attempts = [];
|
|
505
|
-
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
506
|
-
const existingState = await waitForRecommendDetail(client, { timeoutMs: 500 });
|
|
507
|
-
if (!existingState?.popup && !existingState?.resumeIframe) {
|
|
508
|
-
return {
|
|
509
|
-
closed: true,
|
|
510
|
-
attempts
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const rootState = await getRecommendRoots(client);
|
|
515
|
-
const closeTarget = await findVisibleCloseTarget(client, rootState.roots, DETAIL_CLOSE_SELECTORS);
|
|
516
|
-
if (closeTarget) {
|
|
517
|
-
try {
|
|
470
|
+
|
|
471
|
+
const resolved = await findRecommendCardNodeForCandidateKey(client, {
|
|
472
|
+
candidateKey,
|
|
473
|
+
rootState: currentRootState,
|
|
474
|
+
targetUrl,
|
|
475
|
+
timeoutMs: retryTimeoutMs,
|
|
476
|
+
intervalMs: retryIntervalMs
|
|
477
|
+
});
|
|
478
|
+
attempts[attempts.length - 1].refresh_lookup = {
|
|
479
|
+
ok: Boolean(resolved.ok),
|
|
480
|
+
node_id: resolved.node_id || null,
|
|
481
|
+
visible_index: resolved.visible_index ?? null,
|
|
482
|
+
card_count: resolved.card_count || resolved.last_card_count || 0,
|
|
483
|
+
reason: resolved.reason || null,
|
|
484
|
+
error: resolved.error || null
|
|
485
|
+
};
|
|
486
|
+
if (!resolved.ok || !resolved.node_id) {
|
|
487
|
+
error.recommend_detail_open_attempts = attempts;
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
currentNodeId = resolved.node_id;
|
|
491
|
+
currentCandidate = resolved.candidate || currentCandidate;
|
|
492
|
+
currentRootState = resolved.root_state || null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
throw new Error("Recommend detail retry exhausted");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export async function closeRecommendDetail(client, {
|
|
500
|
+
attemptsLimit = 4,
|
|
501
|
+
closeWaitMs = 5000,
|
|
502
|
+
escapeWaitMs = 3500
|
|
503
|
+
} = {}) {
|
|
504
|
+
const attempts = [];
|
|
505
|
+
for (let index = 0; index < attemptsLimit; index += 1) {
|
|
506
|
+
const existingState = await waitForRecommendDetail(client, { timeoutMs: 500 });
|
|
507
|
+
if (!existingState?.popup && !existingState?.resumeIframe) {
|
|
508
|
+
return {
|
|
509
|
+
closed: true,
|
|
510
|
+
attempts
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const rootState = await getRecommendRoots(client);
|
|
515
|
+
const closeTarget = await findVisibleCloseTarget(client, rootState.roots, DETAIL_CLOSE_SELECTORS);
|
|
516
|
+
if (closeTarget) {
|
|
517
|
+
try {
|
|
518
518
|
if (closeTarget.center) {
|
|
519
519
|
await clickPoint(client, closeTarget.center.x, closeTarget.center.y, DETERMINISTIC_CLICK_OPTIONS);
|
|
520
520
|
} else {
|
|
521
521
|
await clickNodeCenter(client, closeTarget.node_id, DETERMINISTIC_CLICK_OPTIONS);
|
|
522
522
|
}
|
|
523
|
-
attempts.push({
|
|
524
|
-
mode: "close-selector",
|
|
525
|
-
selector: closeTarget.selector,
|
|
526
|
-
root: closeTarget.root
|
|
527
|
-
});
|
|
528
|
-
} catch (error) {
|
|
529
|
-
attempts.push({
|
|
530
|
-
mode: "close-selector-error",
|
|
531
|
-
selector: closeTarget.selector,
|
|
532
|
-
root: closeTarget.root,
|
|
533
|
-
error: error?.message || String(error)
|
|
534
|
-
});
|
|
535
|
-
await pressEscape(client);
|
|
536
|
-
attempts.push({ mode: "Escape-after-close-selector-error" });
|
|
537
|
-
}
|
|
538
|
-
} else {
|
|
539
|
-
await pressEscape(client);
|
|
540
|
-
attempts.push({ mode: "Escape" });
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const closedAfterClick = await waitForRecommendDetailClosed(client, {
|
|
544
|
-
timeoutMs: closeWaitMs,
|
|
545
|
-
intervalMs: 250
|
|
546
|
-
});
|
|
547
|
-
attempts.push({
|
|
548
|
-
mode: "wait-closed-after-primary",
|
|
549
|
-
closed: closedAfterClick.closed,
|
|
550
|
-
elapsed_ms: closedAfterClick.elapsed_ms
|
|
551
|
-
});
|
|
523
|
+
attempts.push({
|
|
524
|
+
mode: "close-selector",
|
|
525
|
+
selector: closeTarget.selector,
|
|
526
|
+
root: closeTarget.root
|
|
527
|
+
});
|
|
528
|
+
} catch (error) {
|
|
529
|
+
attempts.push({
|
|
530
|
+
mode: "close-selector-error",
|
|
531
|
+
selector: closeTarget.selector,
|
|
532
|
+
root: closeTarget.root,
|
|
533
|
+
error: error?.message || String(error)
|
|
534
|
+
});
|
|
535
|
+
await pressEscape(client);
|
|
536
|
+
attempts.push({ mode: "Escape-after-close-selector-error" });
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
await pressEscape(client);
|
|
540
|
+
attempts.push({ mode: "Escape" });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const closedAfterClick = await waitForRecommendDetailClosed(client, {
|
|
544
|
+
timeoutMs: closeWaitMs,
|
|
545
|
+
intervalMs: 250
|
|
546
|
+
});
|
|
547
|
+
attempts.push({
|
|
548
|
+
mode: "wait-closed-after-primary",
|
|
549
|
+
closed: closedAfterClick.closed,
|
|
550
|
+
elapsed_ms: closedAfterClick.elapsed_ms
|
|
551
|
+
});
|
|
552
552
|
if (closedAfterClick.closed) {
|
|
553
553
|
return {
|
|
554
554
|
closed: true,
|
|
@@ -578,22 +578,22 @@ export async function closeRecommendDetail(client, {
|
|
|
578
578
|
|
|
579
579
|
await pressEscape(client);
|
|
580
580
|
attempts.push({ mode: "Escape-fallback" });
|
|
581
|
-
|
|
582
|
-
const closedAfterEscape = await waitForRecommendDetailClosed(client, {
|
|
583
|
-
timeoutMs: escapeWaitMs,
|
|
584
|
-
intervalMs: 250
|
|
585
|
-
});
|
|
586
|
-
attempts.push({
|
|
587
|
-
mode: "wait-closed-after-escape",
|
|
588
|
-
closed: closedAfterEscape.closed,
|
|
589
|
-
elapsed_ms: closedAfterEscape.elapsed_ms
|
|
590
|
-
});
|
|
591
|
-
if (closedAfterEscape.closed) {
|
|
592
|
-
return {
|
|
593
|
-
closed: true,
|
|
594
|
-
attempts
|
|
595
|
-
};
|
|
596
|
-
}
|
|
581
|
+
|
|
582
|
+
const closedAfterEscape = await waitForRecommendDetailClosed(client, {
|
|
583
|
+
timeoutMs: escapeWaitMs,
|
|
584
|
+
intervalMs: 250
|
|
585
|
+
});
|
|
586
|
+
attempts.push({
|
|
587
|
+
mode: "wait-closed-after-escape",
|
|
588
|
+
closed: closedAfterEscape.closed,
|
|
589
|
+
elapsed_ms: closedAfterEscape.elapsed_ms
|
|
590
|
+
});
|
|
591
|
+
if (closedAfterEscape.closed) {
|
|
592
|
+
return {
|
|
593
|
+
closed: true,
|
|
594
|
+
attempts
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
const verification = await verifyRecommendDetailStillOpen(client);
|
|
@@ -621,37 +621,37 @@ export async function closeRecommendDetail(client, {
|
|
|
621
621
|
verification
|
|
622
622
|
};
|
|
623
623
|
}
|
|
624
|
-
|
|
625
|
-
async function findVisibleCloseTarget(client, roots, selectors) {
|
|
626
|
-
let fallback = null;
|
|
627
|
-
for (const root of roots) {
|
|
628
|
-
if (!root?.nodeId) continue;
|
|
629
|
-
for (const selector of selectors) {
|
|
630
|
-
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
631
|
-
for (const nodeId of nodeIds) {
|
|
632
|
-
const target = {
|
|
633
|
-
root: root.name,
|
|
634
|
-
root_node_id: root.nodeId,
|
|
635
|
-
selector,
|
|
636
|
-
node_id: nodeId
|
|
637
|
-
};
|
|
638
|
-
if (!fallback) fallback = target;
|
|
639
|
-
try {
|
|
640
|
-
const box = await getNodeBox(client, nodeId);
|
|
641
|
-
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
642
|
-
return {
|
|
643
|
-
...target,
|
|
644
|
-
center: box.center,
|
|
645
|
-
rect: box.rect
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
} catch {}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return fallback;
|
|
653
|
-
}
|
|
654
|
-
|
|
624
|
+
|
|
625
|
+
async function findVisibleCloseTarget(client, roots, selectors) {
|
|
626
|
+
let fallback = null;
|
|
627
|
+
for (const root of roots) {
|
|
628
|
+
if (!root?.nodeId) continue;
|
|
629
|
+
for (const selector of selectors) {
|
|
630
|
+
const nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
631
|
+
for (const nodeId of nodeIds) {
|
|
632
|
+
const target = {
|
|
633
|
+
root: root.name,
|
|
634
|
+
root_node_id: root.nodeId,
|
|
635
|
+
selector,
|
|
636
|
+
node_id: nodeId
|
|
637
|
+
};
|
|
638
|
+
if (!fallback) fallback = target;
|
|
639
|
+
try {
|
|
640
|
+
const box = await getNodeBox(client, nodeId);
|
|
641
|
+
if (box.rect.width > 2 && box.rect.height > 2) {
|
|
642
|
+
return {
|
|
643
|
+
...target,
|
|
644
|
+
center: box.center,
|
|
645
|
+
rect: box.rect
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
} catch {}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return fallback;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
655
|
async function pressEscape(client) {
|
|
656
656
|
await pressKey(client, "Escape", {
|
|
657
657
|
code: "Escape",
|
|
@@ -753,64 +753,64 @@ async function clickOutsideRecommendDetail(client, detailState) {
|
|
|
753
753
|
}
|
|
754
754
|
|
|
755
755
|
export async function extractRecommendDetailCandidate(client, {
|
|
756
|
-
cardCandidate,
|
|
757
|
-
cardNodeId,
|
|
758
|
-
detailState,
|
|
759
|
-
networkEvents = [],
|
|
760
|
-
targetUrl = "",
|
|
761
|
-
closeDetail = true,
|
|
762
|
-
networkParseRetryMs = 1800,
|
|
763
|
-
networkParseIntervalMs = 250
|
|
764
|
-
} = {}) {
|
|
765
|
-
const detailHtml = await readRecommendDetailHtml(client, detailState);
|
|
766
|
-
const detailText = [
|
|
767
|
-
detailHtml.popupText,
|
|
768
|
-
detailHtml.resumeText
|
|
769
|
-
].filter(Boolean).join("\n\n");
|
|
770
|
-
|
|
771
|
-
const parseStarted = Date.now();
|
|
772
|
-
let networkBodies = [];
|
|
773
|
-
let detailCandidateResult = null;
|
|
774
|
-
do {
|
|
775
|
-
networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
|
|
776
|
-
detailCandidateResult = buildScreeningCandidateFromDetail({
|
|
777
|
-
cardCandidate,
|
|
778
|
-
detailText,
|
|
779
|
-
networkBodies,
|
|
780
|
-
metadata: {
|
|
781
|
-
target_url: targetUrl,
|
|
782
|
-
card_node_id: cardNodeId,
|
|
783
|
-
detail_popup_selector: detailState?.popup?.selector || null,
|
|
784
|
-
detail_popup_root: detailState?.popup?.root || null,
|
|
785
|
-
resume_iframe_selector: detailState?.resumeIframe?.selector || null,
|
|
786
|
-
resume_iframe_root: detailState?.resumeIframe?.root || null,
|
|
787
|
-
resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
|
|
788
|
-
detail_html_errors: detailHtml.errors || []
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
|
|
792
|
-
if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
|
|
793
|
-
await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
|
|
794
|
-
} while (true);
|
|
795
|
-
|
|
796
|
-
let closeResult = null;
|
|
797
|
-
if (closeDetail) {
|
|
798
|
-
closeResult = await closeRecommendDetail(client);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
return {
|
|
802
|
-
candidate: detailCandidateResult.candidate,
|
|
803
|
-
parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
|
|
804
|
-
network_bodies: networkBodies,
|
|
805
|
-
network_parse_retry_elapsed_ms: Date.now() - parseStarted,
|
|
806
|
-
network_event_count: networkEvents.length,
|
|
807
|
-
detail: {
|
|
808
|
-
popup_text: detailHtml.popupText,
|
|
809
|
-
resume_text: detailHtml.resumeText,
|
|
810
|
-
popup_html_length: detailHtml.popupHTML.length,
|
|
811
|
-
resume_html_length: detailHtml.resumeHTML.length,
|
|
812
|
-
html_errors: detailHtml.errors || []
|
|
813
|
-
},
|
|
814
|
-
close_result: closeResult
|
|
815
|
-
};
|
|
816
|
-
}
|
|
756
|
+
cardCandidate,
|
|
757
|
+
cardNodeId,
|
|
758
|
+
detailState,
|
|
759
|
+
networkEvents = [],
|
|
760
|
+
targetUrl = "",
|
|
761
|
+
closeDetail = true,
|
|
762
|
+
networkParseRetryMs = 1800,
|
|
763
|
+
networkParseIntervalMs = 250
|
|
764
|
+
} = {}) {
|
|
765
|
+
const detailHtml = await readRecommendDetailHtml(client, detailState);
|
|
766
|
+
const detailText = [
|
|
767
|
+
detailHtml.popupText,
|
|
768
|
+
detailHtml.resumeText
|
|
769
|
+
].filter(Boolean).join("\n\n");
|
|
770
|
+
|
|
771
|
+
const parseStarted = Date.now();
|
|
772
|
+
let networkBodies = [];
|
|
773
|
+
let detailCandidateResult = null;
|
|
774
|
+
do {
|
|
775
|
+
networkBodies = await readRecommendDetailNetworkBodies(client, networkEvents);
|
|
776
|
+
detailCandidateResult = buildScreeningCandidateFromDetail({
|
|
777
|
+
cardCandidate,
|
|
778
|
+
detailText,
|
|
779
|
+
networkBodies,
|
|
780
|
+
metadata: {
|
|
781
|
+
target_url: targetUrl,
|
|
782
|
+
card_node_id: cardNodeId,
|
|
783
|
+
detail_popup_selector: detailState?.popup?.selector || null,
|
|
784
|
+
detail_popup_root: detailState?.popup?.root || null,
|
|
785
|
+
resume_iframe_selector: detailState?.resumeIframe?.selector || null,
|
|
786
|
+
resume_iframe_root: detailState?.resumeIframe?.root || null,
|
|
787
|
+
resume_iframe_document_node_id: detailHtml.resumeIframeDocumentNodeId,
|
|
788
|
+
detail_html_errors: detailHtml.errors || []
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
if (detailCandidateResult.parsed_network_profiles.some((item) => item.ok)) break;
|
|
792
|
+
if (Date.now() - parseStarted >= Math.max(0, Number(networkParseRetryMs) || 0)) break;
|
|
793
|
+
await sleep(Math.max(50, Number(networkParseIntervalMs) || 250));
|
|
794
|
+
} while (true);
|
|
795
|
+
|
|
796
|
+
let closeResult = null;
|
|
797
|
+
if (closeDetail) {
|
|
798
|
+
closeResult = await closeRecommendDetail(client);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
candidate: detailCandidateResult.candidate,
|
|
803
|
+
parsed_network_profiles: detailCandidateResult.parsed_network_profiles,
|
|
804
|
+
network_bodies: networkBodies,
|
|
805
|
+
network_parse_retry_elapsed_ms: Date.now() - parseStarted,
|
|
806
|
+
network_event_count: networkEvents.length,
|
|
807
|
+
detail: {
|
|
808
|
+
popup_text: detailHtml.popupText,
|
|
809
|
+
resume_text: detailHtml.resumeText,
|
|
810
|
+
popup_html_length: detailHtml.popupHTML.length,
|
|
811
|
+
resume_html_length: detailHtml.resumeHTML.length,
|
|
812
|
+
html_errors: detailHtml.errors || []
|
|
813
|
+
},
|
|
814
|
+
close_result: closeResult
|
|
815
|
+
};
|
|
816
|
+
}
|