@reconcrap/boss-recommend-mcp 2.0.31 → 2.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.31",
3
+ "version": "2.0.32",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -24,6 +24,7 @@
24
24
  "test:run-state": "node src/test-run-state.js",
25
25
  "test:cdp-browser": "node src/test-cdp-browser.js",
26
26
  "test:core-capture": "node src/test-core-capture.js",
27
+ "test:core-cv-capture-target": "node src/test-core-cv-capture-target.js",
27
28
  "test:core-cv-acquisition": "node src/test-core-cv-acquisition.js",
28
29
  "test:core-greet-quota": "node src/test-core-greet-quota.js",
29
30
  "test:core-infinite-list": "node src/test-core-infinite-list.js",
@@ -72,6 +73,7 @@
72
73
  "live:chat-domain": "node scripts/live-chat-domain-smoke.js",
73
74
  "live:chat-run-service": "node scripts/live-chat-run-service-smoke.js",
74
75
  "live:chat-mcp": "node scripts/live-chat-mcp-smoke.js",
76
+ "live:cv-capture-target": "node scripts/live-cv-capture-target-smoke.js",
75
77
  "live:chat-phase10-full": "node scripts/live-chat-phase10-full.js",
76
78
  "live:chat-image-screening": "node scripts/live-chat-image-screening-smoke.js"
77
79
  },
package/src/chat-mcp.js CHANGED
@@ -50,6 +50,7 @@ import {
50
50
  resolveBossChatRuntimeLayout,
51
51
  resolveBossScreeningConfig
52
52
  } from "./chat-runtime-config.js";
53
+ import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
53
54
 
54
55
  const DEFAULT_CHAT_HOST = "127.0.0.1";
55
56
  const DEFAULT_CHAT_PORT = 9222;
@@ -1100,7 +1101,7 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "", configRe
1100
1101
  slowLive ? 30000 : 15000
1101
1102
  ),
1102
1103
  resumeDomTimeoutMs: slowLive ? 120000 : 60000,
1103
- maxImagePages: parsePositiveInteger(args.max_image_pages, 8),
1104
+ maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
1104
1105
  imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
1105
1106
  llmConfig: resolvedConfig.ok ? {
1106
1107
  ...resolvedConfig.config
@@ -5,6 +5,7 @@ export const CV_ACQUISITION_MODE_IMAGE = "image";
5
5
  export const NETWORK_RESUME_WAIT_MS = 4200;
6
6
  export const NETWORK_RESUME_RETRY_WAIT_MS = 2000;
7
7
  export const NETWORK_RESUME_IMAGE_MODE_GRACE_MS = 1000;
8
+ export const DEFAULT_MAX_IMAGE_PAGES = 24;
8
9
 
9
10
  const VALID_MODES = new Set([
10
11
  CV_ACQUISITION_MODE_UNKNOWN,
@@ -0,0 +1,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
+ ".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,4 +1,5 @@
1
1
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
2
+ import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
2
3
  import {
3
4
  clickPoint,
4
5
  getNodeBox,
@@ -9,6 +10,7 @@ import {
9
10
  compactCvAcquisitionState,
10
11
  countParsedNetworkProfiles,
11
12
  createCvAcquisitionState,
13
+ DEFAULT_MAX_IMAGE_PAGES,
12
14
  getCvNetworkWaitPlan,
13
15
  recordCvImageFallback,
14
16
  recordCvNetworkHit,
@@ -205,9 +207,9 @@ function llmToScreening(llmResult, candidate) {
205
207
  }
206
208
 
207
209
  export function captureNodeIdFromResumeState(resumeState) {
208
- return resumeState?.popup?.node_id
209
- || resumeState?.content?.node_id
210
+ return resumeState?.content?.node_id
210
211
  || resumeState?.resumeIframe?.node_id
212
+ || resumeState?.popup?.node_id
211
213
  || null;
212
214
  }
213
215
 
@@ -641,7 +643,7 @@ export async function runChatWorkflow({
641
643
  readyTimeoutMs = 60000,
642
644
  onlineResumeButtonTimeoutMs = 30000,
643
645
  resumeDomTimeoutMs = 60000,
644
- maxImagePages = 8,
646
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
645
647
  imageWheelDeltaY = 650,
646
648
  cvAcquisitionMode = "unknown",
647
649
  callLlmOnImage = false,
@@ -1231,11 +1233,19 @@ export async function runChatWorkflow({
1231
1233
  let source = normalizedDetailSource === "dom" ? "dom" : "network";
1232
1234
  let imageEvidence = null;
1233
1235
  let llmResult = null;
1234
- const captureNodeId = captureNodeIdFromResumeState(resumeState);
1236
+ let captureTarget = null;
1237
+ let captureTargetWait = null;
1235
1238
  let fullCvEvidence = summarizeChatFullCvEvidence({ detailResult, contentWait });
1236
1239
  const shouldCaptureImage = normalizedDetailSource === "image"
1237
1240
  || (normalizedDetailSource === "cascade" && !fullCvEvidence.full_cv_acquired);
1238
1241
  if (shouldCaptureImage) {
1242
+ captureTargetWait = await waitForCvCaptureTarget(client, resumeState, {
1243
+ domain: "chat",
1244
+ timeoutMs: 6000,
1245
+ intervalMs: 250
1246
+ });
1247
+ captureTarget = captureTargetWait.target || null;
1248
+ const captureNodeId = captureTarget?.node_id || null;
1239
1249
  if (captureNodeId) {
1240
1250
  detailStep = "capture_image_fallback";
1241
1251
  imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
@@ -1277,7 +1287,9 @@ export async function runChatWorkflow({
1277
1287
  ? "forced_image"
1278
1288
  : "network_miss_image_fallback",
1279
1289
  run_candidate_index: index,
1280
- candidate_key: candidateKey
1290
+ candidate_key: candidateKey,
1291
+ capture_target: captureTarget,
1292
+ capture_target_wait: captureTargetWait
1281
1293
  }
1282
1294
  }));
1283
1295
  source = "image";
@@ -1421,6 +1433,8 @@ export async function runChatWorkflow({
1421
1433
  },
1422
1434
  parsed_network_profile_count: parsedNetworkProfileCount,
1423
1435
  image_evidence: summarizeImageEvidence(imageEvidence),
1436
+ capture_target: captureTarget || null,
1437
+ capture_target_wait: captureTargetWait,
1424
1438
  full_cv_evidence: fullCvEvidence
1425
1439
  };
1426
1440
  }
@@ -1605,7 +1619,7 @@ export function createChatRunService({
1605
1619
  readyTimeoutMs = 60000,
1606
1620
  onlineResumeButtonTimeoutMs = 30000,
1607
1621
  resumeDomTimeoutMs = 60000,
1608
- maxImagePages = 8,
1622
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
1609
1623
  imageWheelDeltaY = 650,
1610
1624
  cvAcquisitionMode = "unknown",
1611
1625
  callLlmOnImage = false,
@@ -20,8 +20,7 @@ import {
20
20
  DETAIL_RESUME_IFRAME_SELECTORS
21
21
  } from "./constants.js";
22
22
  import {
23
- getRecommendRoots,
24
- queryFirstAcrossRoots
23
+ getRecommendRoots
25
24
  } from "./roots.js";
26
25
  import {
27
26
  findRecommendCardNodeIds,
@@ -133,8 +132,8 @@ export async function waitForRecommendDetail(client, {
133
132
  let lastState = null;
134
133
  while (Date.now() - started <= timeoutMs) {
135
134
  const rootState = await getRecommendRoots(client);
136
- const popup = await queryFirstAcrossRoots(client, rootState.roots, DETAIL_POPUP_SELECTORS);
137
- const resumeIframe = await queryFirstAcrossRoots(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
135
+ const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
136
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
138
137
  lastState = {
139
138
  iframe: rootState.iframe,
140
139
  roots: rootState.roots,
@@ -147,6 +146,31 @@ export async function waitForRecommendDetail(client, {
147
146
  return lastState;
148
147
  }
149
148
 
149
+ async function findVisibleDetailTarget(client, roots, selectors) {
150
+ for (const root of roots) {
151
+ if (!root?.nodeId) continue;
152
+ for (const selector of selectors) {
153
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
154
+ for (const nodeId of nodeIds) {
155
+ try {
156
+ const box = await getNodeBox(client, nodeId);
157
+ if (box.rect.width > 2 && box.rect.height > 2) {
158
+ return {
159
+ root: root.name,
160
+ root_node_id: root.nodeId,
161
+ selector,
162
+ node_id: nodeId,
163
+ center: box.center,
164
+ rect: box.rect
165
+ };
166
+ }
167
+ } catch {}
168
+ }
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
150
174
  export async function readRecommendDetailHtml(client, detailState) {
151
175
  let popupHTML = "";
152
176
  let resumeHTML = "";
@@ -7,12 +7,14 @@ import {
7
7
  measureTiming
8
8
  } from "../../core/run/timing.js";
9
9
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
10
+ import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
10
11
  import { sleep } from "../../core/browser/index.js";
11
12
  import { GREET_CREDITS_EXHAUSTED_CODE } from "../../core/greet-quota/index.js";
12
13
  import {
13
14
  compactCvAcquisitionState,
14
15
  countParsedNetworkProfiles,
15
16
  createCvAcquisitionState,
17
+ DEFAULT_MAX_IMAGE_PAGES,
16
18
  getCvNetworkWaitPlan,
17
19
  recordCvImageFallback,
18
20
  recordCvNetworkHit,
@@ -404,7 +406,7 @@ export function createRecoverableImageCaptureEvidence(error, {
404
406
  elapsedMs = 0,
405
407
  filePath = "",
406
408
  extension = "jpg",
407
- maxScreenshots = 8
409
+ maxScreenshots = DEFAULT_MAX_IMAGE_PAGES
408
410
  } = {}) {
409
411
  const filePaths = collectPartialImageEvidencePaths(filePath, extension, maxScreenshots);
410
412
  return {
@@ -474,7 +476,7 @@ export async function runRecommendWorkflow({
474
476
  closeDetail = true,
475
477
  delayMs = 0,
476
478
  cardTimeoutMs = 10000,
477
- maxImagePages = 8,
479
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
478
480
  imageWheelDeltaY = 650,
479
481
  cvAcquisitionMode = "unknown",
480
482
  listMaxScrolls = 20,
@@ -820,15 +822,21 @@ export async function runRecommendWorkflow({
820
822
  const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
821
823
  let source = "network";
822
824
  let imageEvidence = null;
825
+ let captureTarget = null;
826
+ let captureTargetWait = null;
823
827
  if (parsedNetworkProfileCount > 0) {
824
828
  recordCvNetworkHit(cvAcquisitionState, {
825
829
  parsedNetworkProfileCount,
826
830
  waitResult: networkWait
827
831
  });
828
832
  } else {
829
- const captureNodeId = openedDetail.detail_state?.popup?.node_id
830
- || openedDetail.detail_state?.resumeIframe?.node_id
831
- || null;
833
+ captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
834
+ domain: "recommend",
835
+ timeoutMs: 6000,
836
+ intervalMs: 250
837
+ });
838
+ captureTarget = captureTargetWait.target || null;
839
+ const captureNodeId = captureTarget?.node_id || null;
832
840
  if (captureNodeId) {
833
841
  const imageEvidencePath = imageEvidenceFilePath({
834
842
  imageOutputDir,
@@ -844,8 +852,8 @@ export async function runRecommendWorkflow({
844
852
  quality: 72,
845
853
  optimize: true,
846
854
  resizeMaxWidth: 1100,
847
- captureViewport: true,
848
- padding: 4,
855
+ captureViewport: false,
856
+ padding: 0,
849
857
  maxScreenshots: maxImagePages,
850
858
  wheelDeltaY: imageWheelDeltaY,
851
859
  settleMs: 350,
@@ -863,7 +871,9 @@ export async function runRecommendWorkflow({
863
871
  capture_mode: "scroll_sequence",
864
872
  acquisition_reason: "network_miss_image_fallback",
865
873
  run_candidate_index: index,
866
- candidate_key: candidateKey
874
+ candidate_key: candidateKey,
875
+ capture_target: captureTarget,
876
+ capture_target_wait: captureTargetWait
867
877
  }
868
878
  }));
869
879
  source = "image";
@@ -902,7 +912,9 @@ export async function runRecommendWorkflow({
902
912
  wait_plan: waitPlan,
903
913
  network_wait: networkWait,
904
914
  parsed_network_profile_count: parsedNetworkProfileCount,
905
- image_evidence: summarizeImageEvidence(imageEvidence)
915
+ image_evidence: summarizeImageEvidence(imageEvidence),
916
+ capture_target: captureTarget || null,
917
+ capture_target_wait: captureTargetWait
906
918
  };
907
919
  screeningCandidate = detailResult.candidate;
908
920
  } catch (error) {
@@ -1120,7 +1132,7 @@ export function createRecommendRunService({
1120
1132
  closeDetail = true,
1121
1133
  delayMs = 0,
1122
1134
  cardTimeoutMs = 10000,
1123
- maxImagePages = 8,
1135
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
1124
1136
  imageWheelDeltaY = 650,
1125
1137
  cvAcquisitionMode = "unknown",
1126
1138
  listMaxScrolls = 20,
@@ -19,8 +19,7 @@ import {
19
19
  RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS
20
20
  } from "./constants.js";
21
21
  import {
22
- getRecruitRoots,
23
- queryFirstAcrossRoots
22
+ getRecruitRoots
24
23
  } from "./roots.js";
25
24
 
26
25
  export function matchesRecruitDetailNetwork(url) {
@@ -128,8 +127,8 @@ export async function waitForRecruitDetail(client, {
128
127
  let lastState = null;
129
128
  while (Date.now() - started <= timeoutMs) {
130
129
  const rootState = await getRecruitRoots(client);
131
- const popup = await queryFirstAcrossRoots(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
132
- const resumeIframe = await queryFirstAcrossRoots(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
130
+ const popup = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_POPUP_SELECTORS);
131
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, RECRUIT_DETAIL_RESUME_IFRAME_SELECTORS);
133
132
  lastState = {
134
133
  iframe: rootState.iframe,
135
134
  roots: rootState.roots,
@@ -142,6 +141,31 @@ export async function waitForRecruitDetail(client, {
142
141
  return lastState;
143
142
  }
144
143
 
144
+ async function findVisibleDetailTarget(client, roots, selectors) {
145
+ for (const root of roots) {
146
+ if (!root?.nodeId) continue;
147
+ for (const selector of selectors) {
148
+ const nodeIds = await querySelectorAll(client, root.nodeId, selector);
149
+ for (const nodeId of nodeIds) {
150
+ try {
151
+ const box = await getNodeBox(client, nodeId);
152
+ if (box.rect.width > 2 && box.rect.height > 2) {
153
+ return {
154
+ root: root.name,
155
+ root_node_id: root.nodeId,
156
+ selector,
157
+ node_id: nodeId,
158
+ center: box.center,
159
+ rect: box.rect
160
+ };
161
+ }
162
+ } catch {}
163
+ }
164
+ }
165
+ }
166
+ return null;
167
+ }
168
+
145
169
  export async function readRecruitDetailHtml(client, detailState) {
146
170
  let popupHTML = "";
147
171
  let resumeHTML = "";
@@ -5,10 +5,12 @@ import {
5
5
  measureTiming
6
6
  } from "../../core/run/timing.js";
7
7
  import { captureScrolledNodeScreenshots } from "../../core/capture/index.js";
8
+ import { waitForCvCaptureTarget } from "../../core/cv-capture-target/index.js";
8
9
  import {
9
10
  compactCvAcquisitionState,
10
11
  countParsedNetworkProfiles,
11
12
  createCvAcquisitionState,
13
+ DEFAULT_MAX_IMAGE_PAGES,
12
14
  getCvNetworkWaitPlan,
13
15
  recordCvImageFallback,
14
16
  recordCvNetworkHit,
@@ -148,7 +150,7 @@ export async function runRecruitWorkflow({
148
150
  resetBeforeSearch = true,
149
151
  resetTimeoutMs = 180000,
150
152
  cityOptionTimeoutMs = 30000,
151
- maxImagePages = 8,
153
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
152
154
  imageWheelDeltaY = 650,
153
155
  cvAcquisitionMode = "unknown",
154
156
  listMaxScrolls = 20,
@@ -434,15 +436,21 @@ export async function runRecruitWorkflow({
434
436
  const parsedNetworkProfileCount = countParsedNetworkProfiles(detailResult);
435
437
  let source = "network";
436
438
  let imageEvidence = null;
439
+ let captureTarget = null;
440
+ let captureTargetWait = null;
437
441
  if (parsedNetworkProfileCount > 0) {
438
442
  recordCvNetworkHit(cvAcquisitionState, {
439
443
  parsedNetworkProfileCount,
440
444
  waitResult: networkWait
441
445
  });
442
446
  } else {
443
- const captureNodeId = openedDetail.detail_state?.popup?.node_id
444
- || openedDetail.detail_state?.resumeIframe?.node_id
445
- || null;
447
+ captureTargetWait = await waitForCvCaptureTarget(client, openedDetail.detail_state, {
448
+ domain: "recruit",
449
+ timeoutMs: 6000,
450
+ intervalMs: 250
451
+ });
452
+ captureTarget = captureTargetWait.target || null;
453
+ const captureNodeId = captureTarget?.node_id || null;
446
454
  if (captureNodeId) {
447
455
  imageEvidence = await measureTiming(timings, "screenshot_capture_ms", () => captureScrolledNodeScreenshots(client, captureNodeId, {
448
456
  filePath: imageEvidenceFilePath({
@@ -456,8 +464,8 @@ export async function runRecruitWorkflow({
456
464
  quality: 72,
457
465
  optimize: true,
458
466
  resizeMaxWidth: 1100,
459
- captureViewport: true,
460
- padding: 4,
467
+ captureViewport: false,
468
+ padding: 0,
461
469
  maxScreenshots: maxImagePages,
462
470
  wheelDeltaY: imageWheelDeltaY,
463
471
  settleMs: 350,
@@ -475,7 +483,9 @@ export async function runRecruitWorkflow({
475
483
  capture_mode: "scroll_sequence",
476
484
  acquisition_reason: "network_miss_image_fallback",
477
485
  run_candidate_index: index,
478
- candidate_key: candidateKey
486
+ candidate_key: candidateKey,
487
+ capture_target: captureTarget,
488
+ capture_target_wait: captureTargetWait
479
489
  }
480
490
  }));
481
491
  source = "image";
@@ -506,7 +516,9 @@ export async function runRecruitWorkflow({
506
516
  wait_plan: waitPlan,
507
517
  network_wait: networkWait,
508
518
  parsed_network_profile_count: parsedNetworkProfileCount,
509
- image_evidence: summarizeImageEvidence(imageEvidence)
519
+ image_evidence: summarizeImageEvidence(imageEvidence),
520
+ capture_target: captureTarget || null,
521
+ capture_target_wait: captureTargetWait
510
522
  };
511
523
  screeningCandidate = detailResult.candidate;
512
524
  }
@@ -648,7 +660,7 @@ export function createRecruitRunService({
648
660
  resetBeforeSearch = true,
649
661
  resetTimeoutMs = 180000,
650
662
  cityOptionTimeoutMs = 30000,
651
- maxImagePages = 8,
663
+ maxImagePages = DEFAULT_MAX_IMAGE_PAGES,
652
664
  imageWheelDeltaY = 650,
653
665
  cvAcquisitionMode = "unknown",
654
666
  listMaxScrolls = 20,
package/src/index.js CHANGED
@@ -756,7 +756,7 @@ function createBossChatStartInputSchema({ requireFullInput = false } = {}) {
756
756
  max_image_pages: {
757
757
  type: "integer",
758
758
  minimum: 1,
759
- description: "可选,图片简历 fallback 的滚动截图页数上限"
759
+ description: "可选,图片简历 fallback 的滚动截图页数上限,默认 24"
760
760
  },
761
761
  list_max_scrolls: {
762
762
  type: "integer",
@@ -48,6 +48,7 @@ import {
48
48
  resolveBossConfiguredOutputDir,
49
49
  resolveBossScreeningConfig
50
50
  } from "./chat-runtime-config.js";
51
+ import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
51
52
 
52
53
  const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
53
54
  const DEFAULT_RECOMMEND_PORT = 9222;
@@ -1114,7 +1115,7 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1114
1115
  closeDetail: true,
1115
1116
  delayMs: parseNonNegativeInteger(args.delay_ms, 0),
1116
1117
  cardTimeoutMs: slowLive ? 180000 : 90000,
1117
- maxImagePages: parsePositiveInteger(args.max_image_pages, 8),
1118
+ maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
1118
1119
  imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
1119
1120
  cvAcquisitionMode: normalizeText(args.cv_acquisition_mode) || "unknown",
1120
1121
  listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 20),
@@ -36,6 +36,7 @@ import {
36
36
  resolveBossConfiguredOutputDir,
37
37
  resolveBossScreeningConfig
38
38
  } from "./chat-runtime-config.js";
39
+ import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
39
40
 
40
41
  const RUN_MODE_ASYNC = "async";
41
42
  const RUN_MODE_SYNC = "sync";
@@ -855,6 +856,7 @@ function getRunOptions(args, parsed, session, configResolution = null) {
855
856
  resetBeforeSearch: args.reset_search !== false,
856
857
  resetTimeoutMs: slowLive ? 300000 : 180000,
857
858
  cityOptionTimeoutMs: slowLive ? 60000 : 30000,
859
+ maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
858
860
  screeningMode,
859
861
  llmConfig: screeningMode === "llm" && configResolution?.ok ? {
860
862
  ...configResolution.config