@reconcrap/boss-recommend-mcp 2.1.22 → 2.1.24
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 +5 -0
- package/bin/boss-recommend-mcp.js +4 -4
- package/package.json +14 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +43 -42
- package/skills/boss-chat/SKILL.md +107 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +47 -47
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +301 -127
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +291 -114
- package/src/core/capture/index.js +2930 -1201
- package/src/core/cv-acquisition/index.js +512 -238
- package/src/core/cv-capture-target/index.js +513 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +11 -2
- package/src/core/reporting/legacy-csv.js +12 -12
- package/src/core/run/detached-launcher.js +305 -0
- package/src/core/run/index.js +112 -42
- package/src/core/run/timing.js +33 -33
- package/src/core/run/windows-detached-worker.ps1 +106 -0
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +989 -973
- package/src/core/self-heal/viewport.js +1505 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/action-journal.js +443 -0
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +1684 -401
- package/src/domains/chat/index.js +8 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +157 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1801 -760
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +515 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +92 -92
- package/src/domains/recommend/detail.js +12 -3
- package/src/domains/recommend/filters.js +611 -611
- package/src/domains/recommend/index.js +9 -9
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -736
- package/src/domains/recommend/refresh.js +410 -329
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +1783 -592
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1817 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +124 -5
- package/src/parser.js +1296 -1296
- package/src/recommend-mcp.js +515 -80
- package/src/recommend-scheduler.js +66 -0
|
@@ -1,299 +1,513 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getFrameDocumentNodeId,
|
|
3
|
-
getNodeBox,
|
|
4
|
-
querySelector,
|
|
5
|
-
querySelectorAll,
|
|
6
|
-
sleep
|
|
7
|
-
} from "../browser/index.js";
|
|
8
|
-
|
|
9
|
-
export const CV_CAPTURE_TARGET_SELECTORS = Object.freeze([
|
|
10
|
-
".resume-center-side .resume-detail-wrap",
|
|
11
|
-
".resume-container .resume-detail-wrap",
|
|
12
|
-
".resume-container .resume-content-wrap",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
".resume-
|
|
16
|
-
".resume-
|
|
17
|
-
".
|
|
18
|
-
".resume-
|
|
19
|
-
".resume-
|
|
20
|
-
"
|
|
21
|
-
".resume-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
nodeId
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
domain = "",
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1
|
+
import {
|
|
2
|
+
getFrameDocumentNodeId,
|
|
3
|
+
getNodeBox,
|
|
4
|
+
querySelector,
|
|
5
|
+
querySelectorAll,
|
|
6
|
+
sleep
|
|
7
|
+
} from "../browser/index.js";
|
|
8
|
+
|
|
9
|
+
export const CV_CAPTURE_TARGET_SELECTORS = Object.freeze([
|
|
10
|
+
".resume-center-side .resume-detail-wrap",
|
|
11
|
+
".resume-container .resume-detail-wrap",
|
|
12
|
+
".resume-container .resume-content-wrap",
|
|
13
|
+
// Kept for discovery compatibility, but never accepted as a standalone
|
|
14
|
+
// capture target. Boss also uses this class for the narrow action sidebar.
|
|
15
|
+
".resume-item-detail",
|
|
16
|
+
".resume-detail-wrap",
|
|
17
|
+
".resume-content-wrap",
|
|
18
|
+
".resume-common-wrap",
|
|
19
|
+
".new-resume-online-main-ui",
|
|
20
|
+
".resume-detail",
|
|
21
|
+
".resume-recommend",
|
|
22
|
+
"canvas#resume",
|
|
23
|
+
".resume-container"
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const IFRAME_BODY_SELECTORS = Object.freeze(["body", "html"]);
|
|
27
|
+
const STANDALONE_SIDE_PANE_SELECTOR = ".resume-item-detail";
|
|
28
|
+
const DEFAULT_STABILITY_SAMPLES = 2;
|
|
29
|
+
const DEFAULT_STABILITY_INTERVAL_MS = 0;
|
|
30
|
+
const DEFAULT_GEOMETRY_TOLERANCE_PX = 3;
|
|
31
|
+
const DEFAULT_GEOMETRY_TOLERANCE_RATIO = 0.01;
|
|
32
|
+
|
|
33
|
+
const SELECTOR_PRIORITY = new Map([
|
|
34
|
+
[".resume-center-side .resume-detail-wrap", 120],
|
|
35
|
+
[".resume-container .resume-detail-wrap", 115],
|
|
36
|
+
[".resume-container .resume-content-wrap", 110],
|
|
37
|
+
[".resume-detail-wrap", 105],
|
|
38
|
+
[".resume-content-wrap", 100],
|
|
39
|
+
[".resume-common-wrap", 95],
|
|
40
|
+
[".new-resume-online-main-ui", 90],
|
|
41
|
+
[".resume-detail", 85],
|
|
42
|
+
[".resume-recommend", 80],
|
|
43
|
+
["canvas#resume", 75],
|
|
44
|
+
[".resume-container", 70],
|
|
45
|
+
["body", 65],
|
|
46
|
+
["html", 60]
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const SOURCE_PRIORITY = Object.freeze({
|
|
50
|
+
resume_iframe_cv_selector: 700,
|
|
51
|
+
resume_iframe_body: 680,
|
|
52
|
+
resume_iframe_element: 660,
|
|
53
|
+
content_cv_selector: 600,
|
|
54
|
+
content_cv_slot: 580,
|
|
55
|
+
popup_cv_selector: 560,
|
|
56
|
+
popup_cv_slot: 540,
|
|
57
|
+
root_cv_selector: 400
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function slotNodeId(slot = null) {
|
|
61
|
+
return Number(slot?.node_id || slot?.nodeId || 0) || 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rootNodeId(root = null) {
|
|
65
|
+
return Number(root?.nodeId || root?.node_id || root?.root_node_id || 0) || 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeRootName(root = null, fallback = "") {
|
|
69
|
+
return String(root?.name || root?.root || fallback || "").trim() || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function uniqueRoots(roots = []) {
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const root of roots) {
|
|
76
|
+
const nodeId = rootNodeId(root);
|
|
77
|
+
if (!nodeId || seen.has(nodeId)) continue;
|
|
78
|
+
seen.add(nodeId);
|
|
79
|
+
result.push({
|
|
80
|
+
...root,
|
|
81
|
+
name: normalizeRootName(root),
|
|
82
|
+
nodeId
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function slotAsRoot(slot = null, fallbackName = "") {
|
|
89
|
+
const nodeId = slotNodeId(slot);
|
|
90
|
+
if (!nodeId) return null;
|
|
91
|
+
return {
|
|
92
|
+
name: normalizeRootName(slot, fallbackName),
|
|
93
|
+
nodeId,
|
|
94
|
+
selector: slot?.selector || null,
|
|
95
|
+
root_node_id: slot?.root_node_id || null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isVisibleBox(box = null) {
|
|
100
|
+
return box?.rect?.width > 2 && box?.rect?.height > 2;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readVisibleBox(client, nodeId) {
|
|
104
|
+
if (!nodeId) return null;
|
|
105
|
+
try {
|
|
106
|
+
const box = await getNodeBox(client, nodeId);
|
|
107
|
+
return isVisibleBox(box) ? box : null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isCvScopedSelector(selector = "") {
|
|
114
|
+
const normalized = String(selector || "").trim();
|
|
115
|
+
if (!normalized) return false;
|
|
116
|
+
if (normalized === STANDALONE_SIDE_PANE_SELECTOR) return false;
|
|
117
|
+
if (/boss-popup|boss-dialog|dialog-wrap|geek-detail-modal|\bmodal\b|new-chat-resume-dialog-main-ui/i.test(normalized)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return /resume-detail-wrap|resume-content-wrap|resume-common-wrap|new-resume-online-main-ui|resume-detail(?:\b|[.#:])|resume-recommend|canvas#resume|resume-container/i.test(normalized);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isStandaloneSidePaneSelector(selector = "") {
|
|
124
|
+
return String(selector || "").trim() === STANDALONE_SIDE_PANE_SELECTOR;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function selectorPriority(selector = "") {
|
|
128
|
+
return SELECTOR_PRIORITY.get(String(selector || "").trim()) || 10;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sourcePriority(source = "") {
|
|
132
|
+
const normalized = String(source || "").trim();
|
|
133
|
+
if (SOURCE_PRIORITY[normalized]) return SOURCE_PRIORITY[normalized];
|
|
134
|
+
if (normalized.endsWith("_cv_selector")) return 500;
|
|
135
|
+
if (normalized.endsWith("_cv_slot")) return 480;
|
|
136
|
+
return 100;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function semanticPriority(target = null) {
|
|
140
|
+
if (isCvScopedSelector(target?.selector)) return 2;
|
|
141
|
+
if (target?.source === "resume_iframe_body" || target?.source === "resume_iframe_element") return 1;
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function selectionScore(target = null) {
|
|
146
|
+
const width = Math.min(5000, Math.max(0, Number(target?.rect?.width) || 0));
|
|
147
|
+
const height = Math.min(9999, Math.max(0, Number(target?.rect?.height) || 0));
|
|
148
|
+
const area = Math.min(10_000_000, width * height);
|
|
149
|
+
return (
|
|
150
|
+
sourcePriority(target?.source) * 1_000_000_000_000
|
|
151
|
+
+ semanticPriority(target) * 100_000_000_000
|
|
152
|
+
+ width * 10_000_000
|
|
153
|
+
+ area * 1000
|
|
154
|
+
+ selectorPriority(target?.selector)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function compareTargets(left, right) {
|
|
159
|
+
const sourceDelta = sourcePriority(right?.source) - sourcePriority(left?.source);
|
|
160
|
+
if (sourceDelta) return sourceDelta;
|
|
161
|
+
const semanticDelta = semanticPriority(right) - semanticPriority(left);
|
|
162
|
+
if (semanticDelta) return semanticDelta;
|
|
163
|
+
const widthDelta = (Number(right?.rect?.width) || 0) - (Number(left?.rect?.width) || 0);
|
|
164
|
+
if (widthDelta) return widthDelta;
|
|
165
|
+
const rightArea = (Number(right?.rect?.width) || 0) * (Number(right?.rect?.height) || 0);
|
|
166
|
+
const leftArea = (Number(left?.rect?.width) || 0) * (Number(left?.rect?.height) || 0);
|
|
167
|
+
if (rightArea !== leftArea) return rightArea - leftArea;
|
|
168
|
+
const selectorDelta = selectorPriority(right?.selector) - selectorPriority(left?.selector);
|
|
169
|
+
if (selectorDelta) return selectorDelta;
|
|
170
|
+
return Number(left?.node_id || 0) - Number(right?.node_id || 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function targetIdentity(target = null) {
|
|
174
|
+
if (!target?.node_id) return "";
|
|
175
|
+
return [
|
|
176
|
+
target.node_id,
|
|
177
|
+
target.selector || "",
|
|
178
|
+
target.source || "",
|
|
179
|
+
target.root_node_id || "",
|
|
180
|
+
target.iframe_node_id || "",
|
|
181
|
+
target.iframe_document_node_id || ""
|
|
182
|
+
].join("|");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function rectDelta(left = null, right = null) {
|
|
186
|
+
const keys = ["x", "y", "width", "height"];
|
|
187
|
+
return Object.fromEntries(keys.map((key) => [
|
|
188
|
+
key,
|
|
189
|
+
Math.abs((Number(left?.[key]) || 0) - (Number(right?.[key]) || 0))
|
|
190
|
+
]));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function hasStableGeometry(left = null, right = null, {
|
|
194
|
+
geometryTolerancePx = DEFAULT_GEOMETRY_TOLERANCE_PX,
|
|
195
|
+
geometryToleranceRatio = DEFAULT_GEOMETRY_TOLERANCE_RATIO
|
|
196
|
+
} = {}) {
|
|
197
|
+
if (!left || !right) return false;
|
|
198
|
+
const pixelTolerance = Math.max(0, Number(geometryTolerancePx) || 0);
|
|
199
|
+
const ratioTolerance = Math.max(0, Number(geometryToleranceRatio) || 0);
|
|
200
|
+
for (const key of ["x", "y", "width", "height"]) {
|
|
201
|
+
const leftValue = Number(left[key]);
|
|
202
|
+
const rightValue = Number(right[key]);
|
|
203
|
+
if (!Number.isFinite(leftValue) || !Number.isFinite(rightValue)) return false;
|
|
204
|
+
const tolerance = Math.max(
|
|
205
|
+
pixelTolerance,
|
|
206
|
+
Math.max(Math.abs(leftValue), Math.abs(rightValue), 1) * ratioTolerance
|
|
207
|
+
);
|
|
208
|
+
if (Math.abs(leftValue - rightValue) > tolerance) return false;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildTarget({
|
|
214
|
+
domain = "",
|
|
215
|
+
nodeId,
|
|
216
|
+
source,
|
|
217
|
+
selector = null,
|
|
218
|
+
root = null,
|
|
219
|
+
rootNodeId = null,
|
|
220
|
+
box = null,
|
|
221
|
+
iframeNodeId = null,
|
|
222
|
+
iframeDocumentNodeId = null,
|
|
223
|
+
fallback = false,
|
|
224
|
+
containment = null
|
|
225
|
+
} = {}) {
|
|
226
|
+
return {
|
|
227
|
+
schema_version: 1,
|
|
228
|
+
domain: domain || null,
|
|
229
|
+
node_id: nodeId,
|
|
230
|
+
source,
|
|
231
|
+
selector,
|
|
232
|
+
root,
|
|
233
|
+
root_node_id: rootNodeId || null,
|
|
234
|
+
iframe_node_id: iframeNodeId || null,
|
|
235
|
+
iframe_document_node_id: iframeDocumentNodeId || null,
|
|
236
|
+
cv_only: !fallback,
|
|
237
|
+
fallback: Boolean(fallback),
|
|
238
|
+
rect: box?.rect || null,
|
|
239
|
+
center: box?.center || null,
|
|
240
|
+
containment_verified: Boolean(containment),
|
|
241
|
+
containment: containment || null
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function collectVisibleSelectorTargets(client, roots = [], selectors = CV_CAPTURE_TARGET_SELECTORS, {
|
|
246
|
+
domain = "",
|
|
247
|
+
source = "cv_selector",
|
|
248
|
+
iframeNodeId = null,
|
|
249
|
+
iframeDocumentNodeId = null,
|
|
250
|
+
containment = "verified_detail_container",
|
|
251
|
+
selfContainedOnly = false
|
|
252
|
+
} = {}) {
|
|
253
|
+
const targets = [];
|
|
254
|
+
for (const root of uniqueRoots(roots)) {
|
|
255
|
+
for (const selector of selectors) {
|
|
256
|
+
if (isStandaloneSidePaneSelector(selector)) continue;
|
|
257
|
+
if (selfContainedOnly && !isCvScopedSelector(selector)) continue;
|
|
258
|
+
let nodeIds = [];
|
|
259
|
+
try {
|
|
260
|
+
nodeIds = await querySelectorAll(client, root.nodeId, selector);
|
|
261
|
+
} catch {
|
|
262
|
+
nodeIds = [];
|
|
263
|
+
}
|
|
264
|
+
for (const nodeId of nodeIds) {
|
|
265
|
+
const box = await readVisibleBox(client, nodeId);
|
|
266
|
+
if (!box) continue;
|
|
267
|
+
targets.push(buildTarget({
|
|
268
|
+
domain,
|
|
269
|
+
nodeId,
|
|
270
|
+
source,
|
|
271
|
+
selector,
|
|
272
|
+
root: root.name,
|
|
273
|
+
rootNodeId: root.nodeId,
|
|
274
|
+
box,
|
|
275
|
+
iframeNodeId,
|
|
276
|
+
iframeDocumentNodeId,
|
|
277
|
+
containment
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return targets;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function visibleSlotTarget(client, slot = null, {
|
|
286
|
+
domain = "",
|
|
287
|
+
source = "cv_slot",
|
|
288
|
+
fallback = false
|
|
289
|
+
} = {}) {
|
|
290
|
+
const nodeId = slotNodeId(slot);
|
|
291
|
+
if (!nodeId) return null;
|
|
292
|
+
if (!fallback && !isCvScopedSelector(slot?.selector)) return null;
|
|
293
|
+
const box = await readVisibleBox(client, nodeId);
|
|
294
|
+
if (!box) return null;
|
|
295
|
+
return buildTarget({
|
|
296
|
+
domain,
|
|
297
|
+
nodeId,
|
|
298
|
+
source,
|
|
299
|
+
selector: slot?.selector || null,
|
|
300
|
+
root: slot?.root || null,
|
|
301
|
+
rootNodeId: slot?.root_node_id || null,
|
|
302
|
+
box,
|
|
303
|
+
fallback,
|
|
304
|
+
containment: fallback ? null : "verified_cv_slot"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function collectIframeCaptureTargets(client, resumeIframe = null, {
|
|
309
|
+
domain = "",
|
|
310
|
+
selectors = CV_CAPTURE_TARGET_SELECTORS
|
|
311
|
+
} = {}) {
|
|
312
|
+
const iframeNodeId = slotNodeId(resumeIframe);
|
|
313
|
+
if (!iframeNodeId) return [];
|
|
314
|
+
const targets = [];
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const documentNodeId = await getFrameDocumentNodeId(client, iframeNodeId);
|
|
318
|
+
targets.push(...await collectVisibleSelectorTargets(client, [{
|
|
319
|
+
name: "resume-iframe-document",
|
|
320
|
+
nodeId: documentNodeId
|
|
321
|
+
}], selectors, {
|
|
322
|
+
domain,
|
|
323
|
+
source: "resume_iframe_cv_selector",
|
|
324
|
+
iframeNodeId,
|
|
325
|
+
iframeDocumentNodeId: documentNodeId,
|
|
326
|
+
containment: "resume_iframe_document"
|
|
327
|
+
}));
|
|
328
|
+
|
|
329
|
+
for (const selector of IFRAME_BODY_SELECTORS) {
|
|
330
|
+
const nodeId = await querySelector(client, documentNodeId, selector).catch(() => 0);
|
|
331
|
+
const box = await readVisibleBox(client, nodeId);
|
|
332
|
+
if (!box) continue;
|
|
333
|
+
targets.push(buildTarget({
|
|
334
|
+
domain,
|
|
335
|
+
nodeId,
|
|
336
|
+
source: "resume_iframe_body",
|
|
337
|
+
selector,
|
|
338
|
+
root: "resume-iframe-document",
|
|
339
|
+
rootNodeId: documentNodeId,
|
|
340
|
+
box,
|
|
341
|
+
iframeNodeId,
|
|
342
|
+
iframeDocumentNodeId: documentNodeId,
|
|
343
|
+
containment: "resume_iframe_document"
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
} catch {}
|
|
347
|
+
|
|
348
|
+
const iframeBox = await readVisibleBox(client, iframeNodeId);
|
|
349
|
+
if (iframeBox) {
|
|
350
|
+
targets.push(buildTarget({
|
|
351
|
+
domain,
|
|
352
|
+
nodeId: iframeNodeId,
|
|
353
|
+
source: "resume_iframe_element",
|
|
354
|
+
selector: resumeIframe?.selector || null,
|
|
355
|
+
root: resumeIframe?.root || null,
|
|
356
|
+
rootNodeId: resumeIframe?.root_node_id || null,
|
|
357
|
+
box: iframeBox,
|
|
358
|
+
iframeNodeId,
|
|
359
|
+
fallback: false,
|
|
360
|
+
containment: "resume_iframe_element"
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
return targets;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function collectSlotCaptureTargets(client, slot = null, {
|
|
367
|
+
domain = "",
|
|
368
|
+
slotName = "content",
|
|
369
|
+
selectors = CV_CAPTURE_TARGET_SELECTORS
|
|
370
|
+
} = {}) {
|
|
371
|
+
const targets = [];
|
|
372
|
+
const root = slotAsRoot(slot, slotName);
|
|
373
|
+
if (root) {
|
|
374
|
+
targets.push(...await collectVisibleSelectorTargets(client, [root], selectors, {
|
|
375
|
+
domain,
|
|
376
|
+
source: `${slotName}_cv_selector`,
|
|
377
|
+
containment: `${slotName}_detail_container`
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
const slotTarget = await visibleSlotTarget(client, slot, {
|
|
381
|
+
domain,
|
|
382
|
+
source: `${slotName}_cv_slot`
|
|
383
|
+
});
|
|
384
|
+
if (slotTarget) targets.push(slotTarget);
|
|
385
|
+
return targets;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function rankTargets(targets = []) {
|
|
389
|
+
const bestByNode = new Map();
|
|
390
|
+
for (const target of targets) {
|
|
391
|
+
if (!target?.node_id || !target.containment_verified) continue;
|
|
392
|
+
const key = `${target.iframe_document_node_id || 0}|${target.node_id}`;
|
|
393
|
+
const existing = bestByNode.get(key);
|
|
394
|
+
if (!existing || compareTargets(target, existing) < 0) {
|
|
395
|
+
bestByNode.set(key, target);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const ranked = [...bestByNode.values()].sort(compareTargets);
|
|
399
|
+
return ranked.map((target, index) => ({
|
|
400
|
+
...target,
|
|
401
|
+
selection: {
|
|
402
|
+
rank: index + 1,
|
|
403
|
+
candidate_count: ranked.length,
|
|
404
|
+
score: selectionScore(target),
|
|
405
|
+
source_priority: sourcePriority(target.source),
|
|
406
|
+
selector_priority: selectorPriority(target.selector)
|
|
407
|
+
}
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function discoverCvCaptureTargets(client, detailState = null, {
|
|
412
|
+
domain = "",
|
|
413
|
+
selectors = CV_CAPTURE_TARGET_SELECTORS
|
|
414
|
+
} = {}) {
|
|
415
|
+
const targets = [];
|
|
416
|
+
targets.push(...await collectIframeCaptureTargets(client, detailState?.resumeIframe, {
|
|
417
|
+
domain,
|
|
418
|
+
selectors
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
targets.push(...await collectSlotCaptureTargets(client, detailState?.content, {
|
|
422
|
+
domain,
|
|
423
|
+
slotName: "content",
|
|
424
|
+
selectors
|
|
425
|
+
}));
|
|
426
|
+
|
|
427
|
+
targets.push(...await collectSlotCaptureTargets(client, detailState?.popup, {
|
|
428
|
+
domain,
|
|
429
|
+
slotName: "popup",
|
|
430
|
+
selectors
|
|
431
|
+
}));
|
|
432
|
+
|
|
433
|
+
targets.push(...await collectVisibleSelectorTargets(client, detailState?.roots || [], selectors, {
|
|
434
|
+
domain,
|
|
435
|
+
source: "root_cv_selector",
|
|
436
|
+
containment: "self_contained_cv_wrapper",
|
|
437
|
+
selfContainedOnly: true
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
return rankTargets(targets);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function resolveCvCaptureTarget(client, detailState = null, {
|
|
444
|
+
stabilitySamples = DEFAULT_STABILITY_SAMPLES,
|
|
445
|
+
stabilityIntervalMs = DEFAULT_STABILITY_INTERVAL_MS,
|
|
446
|
+
geometryTolerancePx = DEFAULT_GEOMETRY_TOLERANCE_PX,
|
|
447
|
+
geometryToleranceRatio = DEFAULT_GEOMETRY_TOLERANCE_RATIO,
|
|
448
|
+
...options
|
|
449
|
+
} = {}) {
|
|
450
|
+
const requestedSampleCount = Number(stabilitySamples);
|
|
451
|
+
const sampleCount = Number.isFinite(requestedSampleCount)
|
|
452
|
+
? Math.max(DEFAULT_STABILITY_SAMPLES, Math.floor(requestedSampleCount))
|
|
453
|
+
: DEFAULT_STABILITY_SAMPLES;
|
|
454
|
+
const intervalMs = Math.max(0, Number(stabilityIntervalMs) || 0);
|
|
455
|
+
const samples = [];
|
|
456
|
+
let previous = null;
|
|
457
|
+
|
|
458
|
+
for (let index = 0; index < sampleCount; index += 1) {
|
|
459
|
+
const ranked = await discoverCvCaptureTargets(client, detailState, options);
|
|
460
|
+
const current = ranked[0] || null;
|
|
461
|
+
if (!current) return null;
|
|
462
|
+
if (previous) {
|
|
463
|
+
if (targetIdentity(previous) !== targetIdentity(current)) return null;
|
|
464
|
+
if (!hasStableGeometry(previous.rect, current.rect, {
|
|
465
|
+
geometryTolerancePx,
|
|
466
|
+
geometryToleranceRatio
|
|
467
|
+
})) return null;
|
|
468
|
+
}
|
|
469
|
+
samples.push(current.rect);
|
|
470
|
+
previous = current;
|
|
471
|
+
if (index < sampleCount - 1 && intervalMs > 0) await sleep(intervalMs);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const delta = rectDelta(samples[0], samples.at(-1));
|
|
475
|
+
return {
|
|
476
|
+
...previous,
|
|
477
|
+
stability: {
|
|
478
|
+
stable: true,
|
|
479
|
+
sample_count: samples.length,
|
|
480
|
+
interval_ms: intervalMs,
|
|
481
|
+
geometry_tolerance_px: Math.max(0, Number(geometryTolerancePx) || 0),
|
|
482
|
+
geometry_tolerance_ratio: Math.max(0, Number(geometryToleranceRatio) || 0),
|
|
483
|
+
max_rect_delta_px: Math.max(...Object.values(delta)),
|
|
484
|
+
rect_delta: delta
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export async function waitForCvCaptureTarget(client, detailState = null, {
|
|
490
|
+
timeoutMs = 6000,
|
|
491
|
+
intervalMs = 250,
|
|
492
|
+
...options
|
|
493
|
+
} = {}) {
|
|
494
|
+
const started = Date.now();
|
|
495
|
+
let target = null;
|
|
496
|
+
while (Date.now() - started <= Math.max(0, Number(timeoutMs) || 0)) {
|
|
497
|
+
target = await resolveCvCaptureTarget(client, detailState, options);
|
|
498
|
+
if (target?.node_id) {
|
|
499
|
+
return {
|
|
500
|
+
ok: true,
|
|
501
|
+
elapsed_ms: Date.now() - started,
|
|
502
|
+
target
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
await sleep(Math.max(50, Number(intervalMs) || 250));
|
|
506
|
+
}
|
|
507
|
+
target = await resolveCvCaptureTarget(client, detailState, options);
|
|
508
|
+
return {
|
|
509
|
+
ok: Boolean(target?.node_id),
|
|
510
|
+
elapsed_ms: Date.now() - started,
|
|
511
|
+
target: target || null
|
|
512
|
+
};
|
|
513
|
+
}
|