@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25

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.
Files changed (67) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/chat-runtime-config.js +7 -5
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +302 -145
  16. package/src/core/capture/index.js +2930 -1201
  17. package/src/core/cv-acquisition/index.js +512 -238
  18. package/src/core/cv-capture-target/index.js +513 -299
  19. package/src/core/greet-quota/index.js +71 -71
  20. package/src/core/infinite-list/index.js +31 -31
  21. package/src/core/reporting/legacy-csv.js +12 -12
  22. package/src/core/run/detached-launcher.js +305 -0
  23. package/src/core/run/index.js +109 -55
  24. package/src/core/run/timing.js +33 -33
  25. package/src/core/run/windows-detached-worker.ps1 +106 -0
  26. package/src/core/screening/index.js +2400 -2135
  27. package/src/core/self-heal/index.js +989 -973
  28. package/src/core/self-heal/viewport.js +1505 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/action-journal.js +536 -0
  31. package/src/domains/chat/cards.js +137 -137
  32. package/src/domains/chat/constants.js +9 -9
  33. package/src/domains/chat/detail.js +1684 -401
  34. package/src/domains/chat/index.js +8 -7
  35. package/src/domains/chat/jobs.js +620 -620
  36. package/src/domains/chat/page-guard.js +157 -122
  37. package/src/domains/chat/roots.js +56 -56
  38. package/src/domains/chat/run-service.js +1801 -760
  39. package/src/domains/common/account-rights-panel.js +314 -314
  40. package/src/domains/common/recovery-settle.js +159 -159
  41. package/src/domains/recommend/actions.js +1219 -472
  42. package/src/domains/recommend/cards.js +243 -243
  43. package/src/domains/recommend/colleague-contact.js +1079 -333
  44. package/src/domains/recommend/constants.js +92 -92
  45. package/src/domains/recommend/detail.js +4037 -136
  46. package/src/domains/recommend/filters.js +608 -590
  47. package/src/domains/recommend/index.js +9 -9
  48. package/src/domains/recommend/jobs.js +571 -542
  49. package/src/domains/recommend/location.js +754 -707
  50. package/src/domains/recommend/refresh.js +677 -392
  51. package/src/domains/recommend/roots.js +80 -80
  52. package/src/domains/recommend/run-service.js +4048 -1447
  53. package/src/domains/recommend/scopes.js +246 -246
  54. package/src/domains/recruit/actions.js +277 -277
  55. package/src/domains/recruit/cards.js +74 -74
  56. package/src/domains/recruit/constants.js +236 -236
  57. package/src/domains/recruit/detail.js +588 -588
  58. package/src/domains/recruit/index.js +9 -9
  59. package/src/domains/recruit/instruction-parser.js +866 -866
  60. package/src/domains/recruit/refresh.js +45 -45
  61. package/src/domains/recruit/roots.js +68 -68
  62. package/src/domains/recruit/run-service.js +1817 -1620
  63. package/src/domains/recruit/search.js +3229 -3229
  64. package/src/index.js +16 -1
  65. package/src/parser.js +1296 -1296
  66. package/src/recommend-mcp.js +1061 -450
  67. package/src/recommend-scheduler.js +75 -75
@@ -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
- ".resume-item-detail",
14
- ".resume-detail-wrap",
15
- ".resume-content-wrap",
16
- ".resume-common-wrap",
17
- ".new-resume-online-main-ui",
18
- ".resume-detail",
19
- ".resume-recommend",
20
- "canvas#resume",
21
- ".resume-container"
22
- ]);
23
-
24
- const IFRAME_BODY_SELECTORS = Object.freeze(["body", "html"]);
25
-
26
- function slotNodeId(slot = null) {
27
- return Number(slot?.node_id || slot?.nodeId || 0) || 0;
28
- }
29
-
30
- function rootNodeId(root = null) {
31
- return Number(root?.nodeId || root?.node_id || root?.root_node_id || 0) || 0;
32
- }
33
-
34
- function normalizeRootName(root = null, fallback = "") {
35
- return String(root?.name || root?.root || fallback || "").trim() || null;
36
- }
37
-
38
- function uniqueRoots(roots = []) {
39
- const seen = new Set();
40
- const result = [];
41
- for (const root of roots) {
42
- const nodeId = rootNodeId(root);
43
- if (!nodeId || seen.has(nodeId)) continue;
44
- seen.add(nodeId);
45
- result.push({
46
- ...root,
47
- name: normalizeRootName(root),
48
- nodeId
49
- });
50
- }
51
- return result;
52
- }
53
-
54
- function slotAsRoot(slot = null, fallbackName = "") {
55
- const nodeId = slotNodeId(slot);
56
- if (!nodeId) return null;
57
- return {
58
- name: normalizeRootName(slot, fallbackName),
59
- nodeId,
60
- selector: slot?.selector || null,
61
- root_node_id: slot?.root_node_id || null
62
- };
63
- }
64
-
65
- function isVisibleBox(box = null) {
66
- return box?.rect?.width > 2 && box?.rect?.height > 2;
67
- }
68
-
69
- async function readVisibleBox(client, nodeId) {
70
- if (!nodeId) return null;
71
- try {
72
- const box = await getNodeBox(client, nodeId);
73
- return isVisibleBox(box) ? box : null;
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- function isCvScopedSelector(selector = "") {
80
- const normalized = String(selector || "").trim();
81
- if (!normalized) return false;
82
- if (/boss-popup|boss-dialog|dialog-wrap|geek-detail-modal|\bmodal\b|new-chat-resume-dialog-main-ui/i.test(normalized)) {
83
- return false;
84
- }
85
- return /resume-item-detail|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);
86
- }
87
-
88
- function buildTarget({
89
- domain = "",
90
- nodeId,
91
- source,
92
- selector = null,
93
- root = null,
94
- rootNodeId = null,
95
- box = null,
96
- iframeNodeId = null,
97
- iframeDocumentNodeId = null,
98
- fallback = false
99
- } = {}) {
100
- return {
101
- schema_version: 1,
102
- domain: domain || null,
103
- node_id: nodeId,
104
- source,
105
- selector,
106
- root,
107
- root_node_id: rootNodeId || null,
108
- iframe_node_id: iframeNodeId || null,
109
- iframe_document_node_id: iframeDocumentNodeId || null,
110
- cv_only: !fallback,
111
- fallback: Boolean(fallback),
112
- rect: box?.rect || null,
113
- center: box?.center || null
114
- };
115
- }
116
-
117
- async function firstVisibleSelectorTarget(client, roots = [], selectors = CV_CAPTURE_TARGET_SELECTORS, {
118
- domain = "",
119
- source = "cv_selector",
120
- iframeNodeId = null,
121
- iframeDocumentNodeId = null
122
- } = {}) {
123
- for (const root of uniqueRoots(roots)) {
124
- for (const selector of selectors) {
125
- let nodeIds = [];
126
- try {
127
- nodeIds = await querySelectorAll(client, root.nodeId, selector);
128
- } catch {
129
- nodeIds = [];
130
- }
131
- for (const nodeId of nodeIds) {
132
- const box = await readVisibleBox(client, nodeId);
133
- if (!box) continue;
134
- return buildTarget({
135
- domain,
136
- nodeId,
137
- source,
138
- selector,
139
- root: root.name,
140
- rootNodeId: root.nodeId,
141
- box,
142
- iframeNodeId,
143
- iframeDocumentNodeId
144
- });
145
- }
146
- }
147
- }
148
- return null;
149
- }
150
-
151
- async function visibleSlotTarget(client, slot = null, {
152
- domain = "",
153
- source = "cv_slot",
154
- fallback = false
155
- } = {}) {
156
- const nodeId = slotNodeId(slot);
157
- if (!nodeId) return null;
158
- if (!fallback && !isCvScopedSelector(slot?.selector)) return null;
159
- const box = await readVisibleBox(client, nodeId);
160
- if (!box) return null;
161
- return buildTarget({
162
- domain,
163
- nodeId,
164
- source,
165
- selector: slot?.selector || null,
166
- root: slot?.root || null,
167
- rootNodeId: slot?.root_node_id || null,
168
- box,
169
- fallback
170
- });
171
- }
172
-
173
- async function resolveIframeCaptureTarget(client, resumeIframe = null, {
174
- domain = "",
175
- selectors = CV_CAPTURE_TARGET_SELECTORS
176
- } = {}) {
177
- const iframeNodeId = slotNodeId(resumeIframe);
178
- if (!iframeNodeId) return null;
179
-
180
- try {
181
- const documentNodeId = await getFrameDocumentNodeId(client, iframeNodeId);
182
- const selectorTarget = await firstVisibleSelectorTarget(client, [{
183
- name: "resume-iframe-document",
184
- nodeId: documentNodeId
185
- }], selectors, {
186
- domain,
187
- source: "resume_iframe_cv_selector",
188
- iframeNodeId,
189
- iframeDocumentNodeId: documentNodeId
190
- });
191
- if (selectorTarget) return selectorTarget;
192
-
193
- for (const selector of IFRAME_BODY_SELECTORS) {
194
- const nodeId = await querySelector(client, documentNodeId, selector).catch(() => 0);
195
- const box = await readVisibleBox(client, nodeId);
196
- if (!box) continue;
197
- return buildTarget({
198
- domain,
199
- nodeId,
200
- source: "resume_iframe_body",
201
- selector,
202
- root: "resume-iframe-document",
203
- rootNodeId: documentNodeId,
204
- box,
205
- iframeNodeId,
206
- iframeDocumentNodeId: documentNodeId
207
- });
208
- }
209
- } catch {}
210
-
211
- const iframeBox = await readVisibleBox(client, iframeNodeId);
212
- if (!iframeBox) return null;
213
- return buildTarget({
214
- domain,
215
- nodeId: iframeNodeId,
216
- source: "resume_iframe_element",
217
- selector: resumeIframe?.selector || null,
218
- root: resumeIframe?.root || null,
219
- rootNodeId: resumeIframe?.root_node_id || null,
220
- box: iframeBox,
221
- iframeNodeId,
222
- fallback: false
223
- });
224
- }
225
-
226
- async function resolveSlotCaptureTarget(client, slot = null, {
227
- domain = "",
228
- slotName = "content",
229
- selectors = CV_CAPTURE_TARGET_SELECTORS
230
- } = {}) {
231
- const root = slotAsRoot(slot, slotName);
232
- const selectorTarget = root
233
- ? await firstVisibleSelectorTarget(client, [root], selectors, {
234
- domain,
235
- source: `${slotName}_cv_selector`
236
- })
237
- : null;
238
- if (selectorTarget) return selectorTarget;
239
- return visibleSlotTarget(client, slot, {
240
- domain,
241
- source: `${slotName}_cv_slot`
242
- });
243
- }
244
-
245
- export async function resolveCvCaptureTarget(client, detailState = null, {
246
- domain = "",
247
- selectors = CV_CAPTURE_TARGET_SELECTORS
248
- } = {}) {
249
- const iframeTarget = await resolveIframeCaptureTarget(client, detailState?.resumeIframe, {
250
- domain,
251
- selectors
252
- });
253
- if (iframeTarget) return iframeTarget;
254
-
255
- const contentTarget = await resolveSlotCaptureTarget(client, detailState?.content, {
256
- domain,
257
- slotName: "content",
258
- selectors
259
- });
260
- if (contentTarget) return contentTarget;
261
-
262
- const popupTarget = await resolveSlotCaptureTarget(client, detailState?.popup, {
263
- domain,
264
- slotName: "popup",
265
- selectors
266
- });
267
- if (popupTarget) return popupTarget;
268
-
269
- return firstVisibleSelectorTarget(client, detailState?.roots || [], selectors, {
270
- domain,
271
- source: "root_cv_selector"
272
- });
273
- }
274
-
275
- export async function waitForCvCaptureTarget(client, detailState = null, {
276
- timeoutMs = 6000,
277
- intervalMs = 250,
278
- ...options
279
- } = {}) {
280
- const started = Date.now();
281
- let target = null;
282
- while (Date.now() - started <= Math.max(0, Number(timeoutMs) || 0)) {
283
- target = await resolveCvCaptureTarget(client, detailState, options);
284
- if (target?.node_id) {
285
- return {
286
- ok: true,
287
- elapsed_ms: Date.now() - started,
288
- target
289
- };
290
- }
291
- await sleep(Math.max(50, Number(intervalMs) || 250));
292
- }
293
- target = await resolveCvCaptureTarget(client, detailState, options);
294
- return {
295
- ok: Boolean(target?.node_id),
296
- elapsed_ms: Date.now() - started,
297
- target: target || null
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
+ }