@reconcrap/boss-recommend-mcp 2.0.11 → 2.0.13

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.
@@ -6,6 +6,7 @@ import {
6
6
  getAttributesMap,
7
7
  getNodeBox,
8
8
  getOuterHTML,
9
+ querySelectorAll,
9
10
  sleep
10
11
  } from "../browser/index.js";
11
12
  import {
@@ -158,6 +159,185 @@ function screenshotHash(buffer) {
158
159
  return crypto.createHash("sha256").update(buffer).digest("hex");
159
160
  }
160
161
 
162
+ function createCaptureTimeoutError(label, timeoutMs) {
163
+ const error = new Error(`Image fallback capture timed out during ${label} after ${timeoutMs}ms`);
164
+ error.code = "IMAGE_CAPTURE_TIMEOUT";
165
+ error.capture_step = label;
166
+ error.timeout_ms = timeoutMs;
167
+ return error;
168
+ }
169
+
170
+ async function withCaptureTimeout(promise, {
171
+ label = "capture_step",
172
+ timeoutMs = 0
173
+ } = {}) {
174
+ const safeTimeout = Math.max(0, Number(timeoutMs) || 0);
175
+ if (!safeTimeout) return promise;
176
+ let timer = null;
177
+ try {
178
+ return await Promise.race([
179
+ promise,
180
+ new Promise((_, reject) => {
181
+ timer = setTimeout(() => reject(createCaptureTimeoutError(label, safeTimeout)), safeTimeout);
182
+ })
183
+ ]);
184
+ } finally {
185
+ if (timer) clearTimeout(timer);
186
+ }
187
+ }
188
+
189
+ function assertCaptureTotalBudget(started, totalTimeoutMs, label) {
190
+ const safeTimeout = Math.max(0, Number(totalTimeoutMs) || 0);
191
+ if (!safeTimeout) return;
192
+ const elapsed = Date.now() - started;
193
+ if (elapsed <= safeTimeout) return;
194
+ const error = createCaptureTimeoutError(label, safeTimeout);
195
+ error.elapsed_ms = elapsed;
196
+ error.code = "IMAGE_CAPTURE_TOTAL_TIMEOUT";
197
+ throw error;
198
+ }
199
+
200
+ const DEFAULT_SCROLL_ANCHOR_SELECTOR = [
201
+ "h1",
202
+ "h2",
203
+ "h3",
204
+ "h4",
205
+ "h5",
206
+ "p",
207
+ "li",
208
+ "section",
209
+ "article",
210
+ "table",
211
+ "tr",
212
+ "dl",
213
+ "dt",
214
+ "dd",
215
+ "[class*='resume']",
216
+ "[class*='work']",
217
+ "[class*='project']",
218
+ "[class*='education']",
219
+ "[class*='experience']",
220
+ "[class*='item']",
221
+ "div"
222
+ ].join(",");
223
+
224
+ function normalizeScrollMethod(value = "dom-anchor-fallback-input") {
225
+ const normalized = normalizeText(value).toLowerCase();
226
+ if (["dom", "dom-anchor", "dom_anchor", "anchor"].includes(normalized)) return "dom-anchor";
227
+ if (["dom-anchor-fallback-input", "dom_anchor_fallback_input", "dom-fallback-input"].includes(normalized)) {
228
+ return "dom-anchor-fallback-input";
229
+ }
230
+ return "input";
231
+ }
232
+
233
+ function uniqueNumbers(values = []) {
234
+ return Array.from(new Set(values.map((value) => Number(value) || 0).filter(Boolean)));
235
+ }
236
+
237
+ function pickEvenly(items = [], limit = 1) {
238
+ const safeLimit = Math.max(1, Number(limit) || 1);
239
+ if (items.length <= safeLimit) return items;
240
+ const picked = [];
241
+ const last = items.length - 1;
242
+ for (let index = 0; index < safeLimit; index += 1) {
243
+ const sourceIndex = Math.round((index * last) / Math.max(1, safeLimit - 1));
244
+ picked.push(items[sourceIndex]);
245
+ }
246
+ return Array.from(new Map(picked.map((item) => [item.node_id, item])).values());
247
+ }
248
+
249
+ async function collectDomScrollAnchors(client, rootNodeId, {
250
+ selector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
251
+ maxScreenshots = 6,
252
+ maxProbeNodes = 260,
253
+ minAnchorGap = 180,
254
+ stepTimeoutMs = 45000
255
+ } = {}) {
256
+ const started = Date.now();
257
+ let nodeIds = [];
258
+ try {
259
+ nodeIds = uniqueNumbers(await querySelectorAll(client, rootNodeId, selector));
260
+ } catch (error) {
261
+ return {
262
+ ok: false,
263
+ method: "dom-anchor",
264
+ reason: "query_selector_all_failed",
265
+ error: error?.message || String(error)
266
+ };
267
+ }
268
+ if (!nodeIds.length) {
269
+ return {
270
+ ok: false,
271
+ method: "dom-anchor",
272
+ reason: "no_anchor_nodes"
273
+ };
274
+ }
275
+
276
+ const probeLimit = Math.max(1, Number(maxProbeNodes) || 260);
277
+ const perNodeTimeoutMs = Math.min(1200, Math.max(250, Math.floor((Number(stepTimeoutMs) || 45000) / 30)));
278
+ const measured = [];
279
+ for (const nodeId of nodeIds.slice(0, probeLimit)) {
280
+ try {
281
+ const box = await withCaptureTimeout(getNodeBox(client, nodeId), {
282
+ label: `anchor_box_${nodeId}`,
283
+ timeoutMs: perNodeTimeoutMs
284
+ });
285
+ const rect = box?.rect || {};
286
+ if ((Number(rect.width) || 0) < 80 || (Number(rect.height) || 0) < 8) continue;
287
+ measured.push({
288
+ node_id: nodeId,
289
+ y: Math.round(Number(rect.y) || 0),
290
+ height: Math.round(Number(rect.height) || 0)
291
+ });
292
+ } catch {}
293
+ }
294
+
295
+ let anchors = [];
296
+ if (measured.length) {
297
+ const sorted = measured.sort((a, b) => a.y - b.y);
298
+ for (const item of sorted) {
299
+ const last = anchors[anchors.length - 1];
300
+ if (!last || Math.abs(item.y - last.y) >= Math.max(40, Number(minAnchorGap) || 180)) {
301
+ anchors.push(item);
302
+ }
303
+ }
304
+ }
305
+
306
+ if (anchors.length < 2) {
307
+ anchors = nodeIds.slice(0, probeLimit).map((nodeId, index) => ({
308
+ node_id: nodeId,
309
+ y: null,
310
+ height: null,
311
+ document_order: index
312
+ }));
313
+ }
314
+
315
+ anchors = pickEvenly(anchors, Math.max(1, Number(maxScreenshots) || 1));
316
+ return {
317
+ ok: anchors.length > 0,
318
+ method: "dom-anchor",
319
+ elapsed_ms: Date.now() - started,
320
+ selector,
321
+ discovered_node_count: nodeIds.length,
322
+ measured_node_count: measured.length,
323
+ anchor_count: anchors.length,
324
+ anchors
325
+ };
326
+ }
327
+
328
+ async function scrollDomAnchorIntoView(client, nodeId, {
329
+ timeoutMs = 10000,
330
+ label = "dom_scroll_anchor"
331
+ } = {}) {
332
+ if (client.DOM && typeof client.DOM.scrollIntoViewIfNeeded === "function") {
333
+ return withCaptureTimeout(client.DOM.scrollIntoViewIfNeeded({ nodeId }), { label, timeoutMs });
334
+ }
335
+ if (typeof client.send === "function") {
336
+ return withCaptureTimeout(client.send("DOM.scrollIntoViewIfNeeded", { nodeId }), { label, timeoutMs });
337
+ }
338
+ throw new Error("CDP client does not expose DOM.scrollIntoViewIfNeeded");
339
+ }
340
+
161
341
  async function optimizeScreenshotBuffer(buffer, {
162
342
  enabled = false,
163
343
  format = "png",
@@ -339,20 +519,83 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
339
519
  llmPagesPerImage = 3,
340
520
  llmResizeMaxWidth = 1100,
341
521
  llmQuality = 72,
522
+ stepTimeoutMs = 45000,
523
+ totalTimeoutMs = 90000,
524
+ scrollMethod = "dom-anchor-fallback-input",
525
+ scrollAnchorSelector = DEFAULT_SCROLL_ANCHOR_SELECTOR,
526
+ scrollAnchorMaxProbeNodes = 260,
527
+ scrollAnchorMinGap = 180,
342
528
  metadata = {}
343
529
  } = {}) {
344
530
  if (!nodeId) throw new Error("captureScrolledNodeScreenshots requires nodeId");
345
531
  const sequenceStarted = Date.now();
532
+ const normalizedScrollMethod = normalizeScrollMethod(scrollMethod);
533
+ const maxScreenshotCount = Math.max(1, Number(maxScreenshots) || 1);
534
+ const anchorPlan = normalizedScrollMethod !== "input"
535
+ ? await collectDomScrollAnchors(client, nodeId, {
536
+ selector: scrollAnchorSelector,
537
+ maxScreenshots: maxScreenshotCount,
538
+ maxProbeNodes: scrollAnchorMaxProbeNodes,
539
+ minAnchorGap: scrollAnchorMinGap,
540
+ stepTimeoutMs
541
+ })
542
+ : null;
346
543
  const screenshots = [];
347
544
  let consecutiveDuplicates = 0;
348
545
  let previousHash = "";
349
546
  let captureCount = 0;
350
547
  let droppedDuplicateCount = 0;
548
+ let currentScrollMetadata = {
549
+ before_capture: "initial",
550
+ method: normalizedScrollMethod,
551
+ anchor_plan: anchorPlan
552
+ ? {
553
+ ok: Boolean(anchorPlan.ok),
554
+ reason: anchorPlan.reason || null,
555
+ discovered_node_count: anchorPlan.discovered_node_count || 0,
556
+ measured_node_count: anchorPlan.measured_node_count || 0,
557
+ anchor_count: anchorPlan.anchor_count || 0,
558
+ elapsed_ms: anchorPlan.elapsed_ms || 0
559
+ }
560
+ : null
561
+ };
351
562
 
352
- for (let index = 0; index < Math.max(1, Number(maxScreenshots) || 1); index += 1) {
563
+ if (anchorPlan?.anchors?.[0]?.node_id && normalizedScrollMethod !== "input") {
564
+ try {
565
+ await scrollDomAnchorIntoView(client, anchorPlan.anchors[0].node_id, {
566
+ label: "scroll_dom_anchor_initial",
567
+ timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
568
+ });
569
+ currentScrollMetadata = {
570
+ before_capture: "dom_anchor_initial",
571
+ method: "DOM.scrollIntoViewIfNeeded",
572
+ anchor_node_id: anchorPlan.anchors[0].node_id,
573
+ anchor_y: anchorPlan.anchors[0].y,
574
+ anchor_height: anchorPlan.anchors[0].height,
575
+ anchor_plan: currentScrollMetadata.anchor_plan
576
+ };
577
+ } catch (error) {
578
+ if (normalizedScrollMethod === "dom-anchor") {
579
+ throw error;
580
+ }
581
+ currentScrollMetadata = {
582
+ before_capture: "dom_anchor_initial_failed",
583
+ method: "DOM.scrollIntoViewIfNeeded",
584
+ anchor_node_id: anchorPlan.anchors[0].node_id,
585
+ error: error?.message || String(error),
586
+ anchor_plan: currentScrollMetadata.anchor_plan
587
+ };
588
+ }
589
+ }
590
+
591
+ for (let index = 0; index < maxScreenshotCount; index += 1) {
592
+ assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `capture_page_${index + 1}`);
353
593
  captureCount += 1;
354
594
  const captureStarted = Date.now();
355
- const box = await getNodeBox(client, nodeId);
595
+ const box = await withCaptureTimeout(getNodeBox(client, nodeId), {
596
+ label: `get_box_${index + 1}`,
597
+ timeoutMs: stepTimeoutMs
598
+ });
356
599
  const clip = withPadding(box.rect, padding);
357
600
  const captureOptions = captureViewport ? {
358
601
  format,
@@ -367,13 +610,19 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
367
610
  if (quality != null) {
368
611
  captureOptions.quality = quality;
369
612
  }
370
- const screenshot = await client.Page.captureScreenshot(captureOptions);
613
+ const screenshot = await withCaptureTimeout(client.Page.captureScreenshot(captureOptions), {
614
+ label: `capture_screenshot_${index + 1}`,
615
+ timeoutMs: stepTimeoutMs
616
+ });
371
617
  const originalBuffer = Buffer.from(screenshot.data || "", "base64");
372
- const optimized = await optimizeScreenshotBuffer(originalBuffer, {
618
+ const optimized = await withCaptureTimeout(optimizeScreenshotBuffer(originalBuffer, {
373
619
  enabled: optimize,
374
620
  format,
375
621
  quality,
376
622
  resizeMaxWidth
623
+ }), {
624
+ label: `optimize_screenshot_${index + 1}`,
625
+ timeoutMs: stepTimeoutMs
377
626
  });
378
627
  const buffer = optimized.buffer;
379
628
  const hash = screenshotHash(buffer);
@@ -412,9 +661,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
412
661
  clip,
413
662
  capture_viewport: Boolean(captureViewport),
414
663
  node_rect: box.rect,
415
- scroll: index === 0
416
- ? { before_capture: "initial" }
417
- : { before_capture: `wheel_down_${index}` },
664
+ scroll: currentScrollMetadata,
418
665
  metadata
419
666
  });
420
667
  }
@@ -424,27 +671,75 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
424
671
  break;
425
672
  }
426
673
 
427
- if (index < Math.max(1, Number(maxScreenshots) || 1) - 1) {
428
- const x = box.center.x;
429
- const y = box.center.y;
430
- await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" });
431
- await client.Input.dispatchMouseEvent({
432
- type: "mouseWheel",
433
- x,
434
- y,
435
- deltaX: 0,
436
- deltaY: Math.max(1, Number(wheelDeltaY) || 650)
437
- });
674
+ if (index < maxScreenshotCount - 1) {
675
+ assertCaptureTotalBudget(sequenceStarted, totalTimeoutMs, `scroll_after_page_${index + 1}`);
676
+ let scrolledByDomAnchor = false;
677
+ const nextAnchor = anchorPlan?.anchors?.[index + 1] || null;
678
+ if (nextAnchor?.node_id && normalizedScrollMethod !== "input") {
679
+ try {
680
+ await scrollDomAnchorIntoView(client, nextAnchor.node_id, {
681
+ label: `scroll_dom_anchor_${index + 1}`,
682
+ timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
683
+ });
684
+ scrolledByDomAnchor = true;
685
+ currentScrollMetadata = {
686
+ before_capture: `dom_anchor_${index + 1}`,
687
+ method: "DOM.scrollIntoViewIfNeeded",
688
+ anchor_node_id: nextAnchor.node_id,
689
+ anchor_y: nextAnchor.y,
690
+ anchor_height: nextAnchor.height
691
+ };
692
+ } catch (error) {
693
+ if (normalizedScrollMethod === "dom-anchor") {
694
+ throw error;
695
+ }
696
+ currentScrollMetadata = {
697
+ before_capture: `dom_anchor_${index + 1}_failed`,
698
+ method: "DOM.scrollIntoViewIfNeeded",
699
+ anchor_node_id: nextAnchor.node_id,
700
+ error: error?.message || String(error)
701
+ };
702
+ }
703
+ } else if (normalizedScrollMethod === "dom-anchor") {
704
+ break;
705
+ }
706
+
707
+ if (!scrolledByDomAnchor && normalizedScrollMethod !== "dom-anchor") {
708
+ const x = box.center.x;
709
+ const y = box.center.y;
710
+ await withCaptureTimeout(client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" }), {
711
+ label: `scroll_mouse_move_${index + 1}`,
712
+ timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
713
+ });
714
+ await withCaptureTimeout(client.Input.dispatchMouseEvent({
715
+ type: "mouseWheel",
716
+ x,
717
+ y,
718
+ deltaX: 0,
719
+ deltaY: Math.max(1, Number(wheelDeltaY) || 650)
720
+ }), {
721
+ label: `scroll_wheel_${index + 1}`,
722
+ timeoutMs: Math.min(Math.max(3000, Number(stepTimeoutMs) || 45000), 10000)
723
+ });
724
+ currentScrollMetadata = {
725
+ before_capture: `wheel_down_${index + 1}`,
726
+ method: "Input.dispatchMouseEvent",
727
+ fallback_from_dom_anchor: Boolean(anchorPlan && normalizedScrollMethod === "dom-anchor-fallback-input")
728
+ };
729
+ }
438
730
  if (settleMs > 0) await sleep(settleMs);
439
731
  }
440
732
  }
441
733
 
442
734
  const llmComposition = composeForLlm
443
- ? await composeScreenshotsForLlm(screenshots, {
735
+ ? await withCaptureTimeout(composeScreenshotsForLlm(screenshots, {
444
736
  basePath: filePath,
445
737
  pagesPerImage: llmPagesPerImage,
446
738
  resizeMaxWidth: llmResizeMaxWidth,
447
739
  quality: llmQuality
740
+ }), {
741
+ label: "compose_llm_screenshots",
742
+ timeoutMs: stepTimeoutMs
448
743
  })
449
744
  : {
450
745
  llm_file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
@@ -456,6 +751,7 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
456
751
 
457
752
  return {
458
753
  schema_version: 1,
754
+ ok: true,
459
755
  source: "image-scroll-sequence",
460
756
  captured_at: nowIso(),
461
757
  node_id: nodeId,
@@ -482,8 +778,15 @@ export async function captureScrolledNodeScreenshots(client, nodeId, {
482
778
  llm_compose_enabled: Boolean(composeForLlm),
483
779
  llm_pages_per_image: Math.max(1, Math.min(5, Number(llmPagesPerImage) || 3)),
484
780
  llm_resize_max_width: Math.max(0, Number(llmResizeMaxWidth) || 0),
485
- llm_quality: llmQuality ?? null
781
+ llm_quality: llmQuality ?? null,
782
+ step_timeout_ms: Math.max(0, Number(stepTimeoutMs) || 0),
783
+ total_timeout_ms: Math.max(0, Number(totalTimeoutMs) || 0),
784
+ scroll_method: normalizedScrollMethod,
785
+ scroll_anchor_selector: scrollAnchorSelector,
786
+ scroll_anchor_max_probe_nodes: Math.max(1, Number(scrollAnchorMaxProbeNodes) || 260),
787
+ scroll_anchor_min_gap: Math.max(0, Number(scrollAnchorMinGap) || 0)
486
788
  },
789
+ scroll_anchor_plan: anchorPlan,
487
790
  file_paths: screenshots.map((item) => item.file_path).filter(Boolean),
488
791
  screenshots,
489
792
  metadata
@@ -124,6 +124,7 @@ export function hasParsedNetworkProfile(detailResult = {}) {
124
124
  export function summarizeImageEvidence(imageEvidence = null) {
125
125
  if (!imageEvidence) return null;
126
126
  return {
127
+ ok: imageEvidence.ok !== false,
127
128
  source: imageEvidence.source || "",
128
129
  elapsed_ms: imageEvidence.elapsed_ms || 0,
129
130
  capture_count: imageEvidence.capture_count || imageEvidence.screenshot_count || 0,
@@ -137,6 +138,8 @@ export function summarizeImageEvidence(imageEvidence = null) {
137
138
  llm_original_total_byte_length: imageEvidence.llm_original_total_byte_length || 0,
138
139
  llm_composition_error: imageEvidence.llm_composition_error || null,
139
140
  optimization: imageEvidence.optimization || null,
141
+ error_code: imageEvidence.error_code || imageEvidence.code || null,
142
+ error: imageEvidence.error || null,
140
143
  file_paths: imageEvidence.file_paths || [],
141
144
  llm_file_paths: imageEvidence.llm_file_paths || [],
142
145
  first_clip: imageEvidence.screenshots?.[0]?.clip || imageEvidence.clip || null