@reconcrap/boss-recommend-mcp 2.0.25 → 2.0.26

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.25",
3
+ "version": "2.0.26",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -30,6 +30,7 @@ export const DEFAULT_LOAD_MORE_HINT_KEYWORDS = Object.freeze([
30
30
 
31
31
  export const DEFAULT_BOTTOM_MARKER_SELECTORS = Object.freeze([
32
32
  ".finished-wrap",
33
+ ".loadmore",
33
34
  ".load-tips",
34
35
  "div[role=\"tfoot\"] .load-tips",
35
36
  ".no-data-refresh",
@@ -38,6 +39,7 @@ export const DEFAULT_BOTTOM_MARKER_SELECTORS = Object.freeze([
38
39
  ".no-data",
39
40
  ".tip-nodata",
40
41
  "[class*=\"finished\"]",
42
+ "[class*=\"loadmore\"]",
41
43
  "[class*=\"load-tips\"]",
42
44
  "[class*=\"no-more\"]",
43
45
  "[class*=\"no_more\"]"
@@ -96,6 +98,345 @@ function isUsableBox(box) {
96
98
  return Number(box?.rect?.width || 0) > 2 && Number(box?.rect?.height || 0) > 2;
97
99
  }
98
100
 
101
+ function isUsableRect(rect) {
102
+ return Number(rect?.width || 0) > 2 && Number(rect?.height || 0) > 2;
103
+ }
104
+
105
+ function pointFromRect(rect, {
106
+ xRatio = 0.5,
107
+ yRatio = 0.75,
108
+ inset = 8
109
+ } = {}) {
110
+ if (!isUsableRect(rect)) return null;
111
+ const safeInsetX = Math.min(Math.max(0, Number(inset) || 0), Math.max(0, rect.width / 2 - 1));
112
+ const safeInsetY = Math.min(Math.max(0, Number(inset) || 0), Math.max(0, rect.height / 2 - 1));
113
+ const minX = rect.x + safeInsetX;
114
+ const maxX = rect.x + rect.width - safeInsetX;
115
+ const minY = rect.y + safeInsetY;
116
+ const maxY = rect.y + rect.height - safeInsetY;
117
+ return {
118
+ x: Math.min(maxX, Math.max(minX, rect.x + rect.width * (Number(xRatio) || 0.5))),
119
+ y: Math.min(maxY, Math.max(minY, rect.y + rect.height * (Number(yRatio) || 0.75)))
120
+ };
121
+ }
122
+
123
+ function unionRects(rects = []) {
124
+ const usable = rects.filter(isUsableRect);
125
+ if (!usable.length) return null;
126
+ const left = Math.min(...usable.map((rect) => rect.x));
127
+ const top = Math.min(...usable.map((rect) => rect.y));
128
+ const right = Math.max(...usable.map((rect) => rect.x + rect.width));
129
+ const bottom = Math.max(...usable.map((rect) => rect.y + rect.height));
130
+ return {
131
+ x: left,
132
+ y: top,
133
+ width: right - left,
134
+ height: bottom - top
135
+ };
136
+ }
137
+
138
+ function pointInsideRect(point, rect, {
139
+ padding = 0
140
+ } = {}) {
141
+ if (!point || !isUsableRect(rect)) return false;
142
+ const pad = Math.max(0, Number(padding) || 0);
143
+ return Number(point.x) >= rect.x + pad
144
+ && Number(point.x) <= rect.x + rect.width - pad
145
+ && Number(point.y) >= rect.y + pad
146
+ && Number(point.y) <= rect.y + rect.height - pad;
147
+ }
148
+
149
+ function rectsIntersect(a, b, {
150
+ padding = 0
151
+ } = {}) {
152
+ if (!isUsableRect(a) || !isUsableRect(b)) return false;
153
+ const pad = Math.max(0, Number(padding) || 0);
154
+ return a.x + a.width >= b.x + pad
155
+ && b.x + b.width >= a.x + pad
156
+ && a.y + a.height >= b.y + pad
157
+ && b.y + b.height >= a.y + pad;
158
+ }
159
+
160
+ function intersectRects(a, b) {
161
+ if (!isUsableRect(a) || !isUsableRect(b)) return null;
162
+ const left = Math.max(a.x, b.x);
163
+ const top = Math.max(a.y, b.y);
164
+ const right = Math.min(a.x + a.width, b.x + b.width);
165
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
166
+ if (right - left <= 2 || bottom - top <= 2) return null;
167
+ return {
168
+ x: left,
169
+ y: top,
170
+ width: right - left,
171
+ height: bottom - top
172
+ };
173
+ }
174
+
175
+ function normalizePoint(point) {
176
+ if (!point) return null;
177
+ const x = Number(point.x);
178
+ const y = Number(point.y);
179
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
180
+ return { x, y };
181
+ }
182
+
183
+ function resolveViewportPoint(viewportPoint, viewport) {
184
+ if (!viewportPoint) return null;
185
+ if (viewport && ("xRatio" in viewportPoint || "yRatio" in viewportPoint)) {
186
+ const xRatio = Number(viewportPoint.xRatio ?? 0);
187
+ const yRatio = Number(viewportPoint.yRatio ?? 0);
188
+ if (Number.isFinite(xRatio) && Number.isFinite(yRatio)) {
189
+ return {
190
+ x: viewport.x + viewport.width * xRatio,
191
+ y: viewport.y + viewport.height * yRatio
192
+ };
193
+ }
194
+ }
195
+ return normalizePoint(viewportPoint);
196
+ }
197
+
198
+ async function getViewportRect(client) {
199
+ try {
200
+ const metrics = await client.Page.getLayoutMetrics();
201
+ const viewport = metrics.visualViewport || metrics.layoutViewport || metrics.cssVisualViewport || {};
202
+ const width = Number(viewport.clientWidth || viewport.width || metrics.layoutViewport?.clientWidth || 0);
203
+ const height = Number(viewport.clientHeight || viewport.height || metrics.layoutViewport?.clientHeight || 0);
204
+ const x = Number(viewport.pageX || viewport.x || 0);
205
+ const y = Number(viewport.pageY || viewport.y || 0);
206
+ if (width > 0 && height > 0) {
207
+ return { x, y, width, height };
208
+ }
209
+ } catch {
210
+ // Page.getLayoutMetrics is optional for fallback only.
211
+ }
212
+ return null;
213
+ }
214
+
215
+ async function collectUsableNodeBoxes(client, nodeIds = [], {
216
+ maxNodes = 80
217
+ } = {}) {
218
+ const boxes = [];
219
+ const errors = [];
220
+ for (const nodeId of nodeIds.slice(0, Math.max(1, Number(maxNodes) || 80))) {
221
+ try {
222
+ const box = await getNodeBox(client, nodeId);
223
+ if (isUsableBox(box)) {
224
+ boxes.push({
225
+ node_id: nodeId,
226
+ box,
227
+ rect: box.rect
228
+ });
229
+ }
230
+ } catch (error) {
231
+ errors.push({
232
+ node_id: nodeId,
233
+ error: error?.message || String(error)
234
+ });
235
+ }
236
+ }
237
+ return { boxes, errors };
238
+ }
239
+
240
+ async function querySelectorBoxes(client, rootNodeId, selectors = [], {
241
+ maxNodes = 80
242
+ } = {}) {
243
+ const attempts = [];
244
+ if (!rootNodeId) return { boxes: [], attempts };
245
+ for (const selector of selectors.filter(Boolean)) {
246
+ let nodeIds = [];
247
+ try {
248
+ nodeIds = await querySelectorAll(client, rootNodeId, selector);
249
+ } catch (error) {
250
+ attempts.push({
251
+ selector,
252
+ error: error?.message || String(error),
253
+ node_count: 0,
254
+ box_count: 0
255
+ });
256
+ continue;
257
+ }
258
+ const measured = await collectUsableNodeBoxes(client, nodeIds, { maxNodes });
259
+ attempts.push({
260
+ selector,
261
+ node_count: nodeIds.length,
262
+ box_count: measured.boxes.length,
263
+ errors: measured.errors
264
+ });
265
+ if (measured.boxes.length) {
266
+ return {
267
+ boxes: measured.boxes,
268
+ selector,
269
+ attempts
270
+ };
271
+ }
272
+ }
273
+ return { boxes: [], attempts };
274
+ }
275
+
276
+ export async function resolveInfiniteListFallbackPoint(client, {
277
+ rootNodeId = 0,
278
+ containerSelectors = [],
279
+ itemNodeIds = [],
280
+ itemSelectors = [],
281
+ allowedSources = ["container", "item_union", "viewport_ratio"],
282
+ containerXRatio = 0.5,
283
+ containerYRatio = 0.5,
284
+ itemXRatio = 0.5,
285
+ itemYRatio = 0.5,
286
+ viewportPoint = null,
287
+ validateViewportPoint = true,
288
+ maxProbeNodes = 80
289
+ } = {}) {
290
+ const attempts = [];
291
+ const allowed = new Set(Array.isArray(allowedSources) && allowedSources.length
292
+ ? allowedSources.map((source) => String(source || ""))
293
+ : ["container", "item_union", "viewport_ratio"]);
294
+
295
+ const containerResult = await querySelectorBoxes(client, rootNodeId, containerSelectors, {
296
+ maxNodes: maxProbeNodes
297
+ });
298
+ attempts.push({
299
+ source: "container",
300
+ selector: containerResult.selector || null,
301
+ attempts: containerResult.attempts
302
+ });
303
+ const containerBox = containerResult.boxes
304
+ .slice()
305
+ .sort((a, b) => (b.rect.width * b.rect.height) - (a.rect.width * a.rect.height))[0];
306
+ const viewport = await getViewportRect(client);
307
+ const inputViewportRect = viewport
308
+ ? { x: 0, y: 0, width: viewport.width, height: viewport.height }
309
+ : null;
310
+ const visibleContainerRect = inputViewportRect && containerBox?.rect
311
+ ? intersectRects(containerBox.rect, inputViewportRect) || containerBox.rect
312
+ : containerBox?.rect;
313
+ const containerPoint = pointFromRect(visibleContainerRect, {
314
+ xRatio: containerXRatio,
315
+ yRatio: containerYRatio
316
+ });
317
+ if (containerPoint && allowed.has("container")) {
318
+ return {
319
+ ok: true,
320
+ source: "container",
321
+ point: containerPoint,
322
+ selector: containerResult.selector || null,
323
+ node_id: containerBox.node_id,
324
+ assist_node_id: itemNodeIds.slice(-1)[0] || null,
325
+ rect: visibleContainerRect,
326
+ full_rect: containerBox.rect,
327
+ attempts
328
+ };
329
+ }
330
+
331
+ let itemBoxes = [];
332
+ const itemProbeNodeIds = itemNodeIds.length > maxProbeNodes
333
+ ? itemNodeIds.slice(-maxProbeNodes)
334
+ : itemNodeIds;
335
+ const measuredItems = await collectUsableNodeBoxes(client, itemProbeNodeIds, { maxNodes: maxProbeNodes });
336
+ itemBoxes = measuredItems.boxes;
337
+ attempts.push({
338
+ source: "visible_items",
339
+ node_count: itemNodeIds.length,
340
+ box_count: measuredItems.boxes.length,
341
+ errors: measuredItems.errors
342
+ });
343
+ if (!itemBoxes.length) {
344
+ const queriedItems = await querySelectorBoxes(client, rootNodeId, itemSelectors, {
345
+ maxNodes: maxProbeNodes
346
+ });
347
+ itemBoxes = queriedItems.boxes;
348
+ attempts.push({
349
+ source: "item_selector",
350
+ selector: queriedItems.selector || null,
351
+ attempts: queriedItems.attempts
352
+ });
353
+ }
354
+ const itemValidationRects = [
355
+ inputViewportRect,
356
+ visibleContainerRect || containerBox?.rect || null
357
+ ].filter(isUsableRect);
358
+ const visibleItemBoxes = itemValidationRects.length
359
+ ? itemBoxes.filter((item) => itemValidationRects.every((rect) => rectsIntersect(item.rect, rect, { padding: 1 })))
360
+ : itemBoxes;
361
+ attempts.push({
362
+ source: "visible_item_filter",
363
+ input_box_count: itemBoxes.length,
364
+ output_box_count: visibleItemBoxes.length,
365
+ validation_rect_count: itemValidationRects.length
366
+ });
367
+ const unionSourceBoxes = visibleItemBoxes.length ? visibleItemBoxes : itemBoxes;
368
+ const rawItemUnion = unionRects(unionSourceBoxes.map((item) => item.rect));
369
+ const itemUnion = itemValidationRects.reduce(
370
+ (rect, limit) => intersectRects(rect, limit) || rect,
371
+ rawItemUnion
372
+ );
373
+ const itemPoint = pointFromRect(itemUnion, {
374
+ xRatio: itemXRatio,
375
+ yRatio: itemYRatio
376
+ });
377
+ if (itemPoint && allowed.has("item_union")) {
378
+ const assistItem = unionSourceBoxes
379
+ .slice()
380
+ .sort((a, b) => ((b.rect.y + b.rect.height) - (a.rect.y + a.rect.height)))[0];
381
+ return {
382
+ ok: true,
383
+ source: "item_union",
384
+ point: itemPoint,
385
+ rect: itemUnion,
386
+ full_rect: rawItemUnion,
387
+ item_box_count: unionSourceBoxes.length,
388
+ visible_item_box_count: visibleItemBoxes.length,
389
+ assist_node_id: assistItem?.node_id || itemNodeIds.slice(-1)[0] || null,
390
+ attempts
391
+ };
392
+ }
393
+
394
+ const viewportRatioPoint = resolveViewportPoint(viewportPoint, viewport);
395
+ const normalizedViewportPoint = normalizePoint(viewportRatioPoint);
396
+ if (normalizedViewportPoint && allowed.has("viewport_ratio")) {
397
+ if (!validateViewportPoint) {
398
+ return {
399
+ ok: true,
400
+ source: "viewport_ratio",
401
+ point: normalizedViewportPoint,
402
+ viewport,
403
+ validated: false,
404
+ attempts
405
+ };
406
+ }
407
+ const validationRects = [
408
+ ...containerResult.boxes.map((item) => item.rect),
409
+ ...itemBoxes.map((item) => item.rect)
410
+ ].filter(isUsableRect);
411
+ const validatedRect = validationRects.find((rect) => pointInsideRect(normalizedViewportPoint, rect, { padding: 4 }));
412
+ attempts.push({
413
+ source: "viewport_ratio",
414
+ point: normalizedViewportPoint,
415
+ viewport,
416
+ validation_rect_count: validationRects.length,
417
+ validated: Boolean(validatedRect)
418
+ });
419
+ if (validatedRect) {
420
+ return {
421
+ ok: true,
422
+ source: "viewport_ratio",
423
+ point: normalizedViewportPoint,
424
+ viewport,
425
+ rect: validatedRect,
426
+ validated: true,
427
+ assist_node_id: itemNodeIds.slice(-1)[0] || null,
428
+ attempts
429
+ };
430
+ }
431
+ }
432
+
433
+ return {
434
+ ok: false,
435
+ reason: "fallback_point_unavailable",
436
+ attempts
437
+ };
438
+ }
439
+
99
440
  function shortHash(value) {
100
441
  return crypto.createHash("sha256").update(String(value || "")).digest("hex").slice(0, 16);
101
442
  }
@@ -561,6 +902,29 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
561
902
  }
562
903
 
563
904
  const errors = [];
905
+ const wheelDelta = Math.max(1, Number(wheelDeltaY) || 850);
906
+ async function synthesizeGesture(x, y) {
907
+ if (typeof client?.Input?.synthesizeScrollGesture !== "function") return null;
908
+ try {
909
+ const gestureDistance = -Math.min(1200, wheelDelta);
910
+ await client.Input.synthesizeScrollGesture({
911
+ x,
912
+ y,
913
+ yDistance: gestureDistance,
914
+ speed: 800,
915
+ repeatCount: 1
916
+ });
917
+ return {
918
+ ok: true,
919
+ y_distance: gestureDistance
920
+ };
921
+ } catch (error) {
922
+ return {
923
+ ok: false,
924
+ error: error?.message || String(error)
925
+ };
926
+ }
927
+ }
564
928
  for (const anchor of candidates.slice().reverse()) {
565
929
  try {
566
930
  await scrollNodeIntoView(client, anchor.node_id);
@@ -574,15 +938,17 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
574
938
  x,
575
939
  y,
576
940
  deltaX: 0,
577
- deltaY: Math.max(1, Number(wheelDeltaY) || 850)
941
+ deltaY: wheelDelta
578
942
  });
943
+ const gesture = await synthesizeGesture(x, y);
579
944
  if (settleMs > 0) await sleep(settleMs);
580
945
  return {
581
946
  ok: true,
582
947
  anchor_key: anchor.key,
583
948
  anchor_node_id: anchor.node_id,
584
949
  point: { x, y },
585
- wheel_delta_y: Math.max(1, Number(wheelDeltaY) || 850),
950
+ wheel_delta_y: wheelDelta,
951
+ gesture,
586
952
  settle_ms: settleMs,
587
953
  skipped_stale_anchor_count: errors.length
588
954
  };
@@ -595,23 +961,56 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
595
961
  }
596
962
  }
597
963
 
598
- if (fallbackPoint && Number.isFinite(Number(fallbackPoint.x)) && Number.isFinite(Number(fallbackPoint.y))) {
599
- const x = Number(fallbackPoint.x);
600
- const y = Number(fallbackPoint.y);
964
+ const resolvedFallback = typeof fallbackPoint === "function"
965
+ ? await fallbackPoint({ client, items, errors })
966
+ : (fallbackPoint ? { ok: true, source: "static", point: fallbackPoint } : null);
967
+ const resolvedPoint = normalizePoint(resolvedFallback?.point || resolvedFallback);
968
+ if (resolvedPoint) {
969
+ const x = resolvedPoint.x;
970
+ const y = resolvedPoint.y;
971
+ let assist = null;
972
+ if (resolvedFallback?.assist_node_id) {
973
+ try {
974
+ await scrollNodeIntoView(client, resolvedFallback.assist_node_id);
975
+ await sleep(150);
976
+ assist = {
977
+ ok: true,
978
+ node_id: resolvedFallback.assist_node_id
979
+ };
980
+ } catch (error) {
981
+ assist = {
982
+ ok: false,
983
+ node_id: resolvedFallback.assist_node_id,
984
+ error: error?.message || String(error)
985
+ };
986
+ }
987
+ }
601
988
  await client.Input.dispatchMouseEvent({ type: "mouseMoved", x, y, button: "none" });
602
989
  await client.Input.dispatchMouseEvent({
603
990
  type: "mouseWheel",
604
991
  x,
605
992
  y,
606
993
  deltaX: 0,
607
- deltaY: Math.max(1, Number(wheelDeltaY) || 850)
994
+ deltaY: wheelDelta
608
995
  });
996
+ const gesture = await synthesizeGesture(x, y);
609
997
  if (settleMs > 0) await sleep(settleMs);
610
998
  return {
611
999
  ok: true,
612
1000
  mode: "fallback_point",
1001
+ fallback: {
1002
+ source: resolvedFallback?.source || "static",
1003
+ selector: resolvedFallback?.selector || null,
1004
+ node_id: resolvedFallback?.node_id || null,
1005
+ assist_node_id: resolvedFallback?.assist_node_id || null,
1006
+ rect: resolvedFallback?.rect || null,
1007
+ validated: resolvedFallback?.validated ?? null,
1008
+ reason: resolvedFallback?.reason || null
1009
+ },
1010
+ assist,
613
1011
  point: { x, y },
614
- wheel_delta_y: Math.max(1, Number(wheelDeltaY) || 850),
1012
+ wheel_delta_y: wheelDelta,
1013
+ gesture,
615
1014
  settle_ms: settleMs,
616
1015
  skipped_stale_anchor_count: errors.length,
617
1016
  stale_anchor_errors: errors
@@ -621,7 +1020,8 @@ export async function scrollInfiniteListByVisibleItems(client, items = [], {
621
1020
  return {
622
1021
  ok: false,
623
1022
  reason: "scroll_anchor_unavailable",
624
- errors
1023
+ errors,
1024
+ fallback: resolvedFallback || null
625
1025
  };
626
1026
  }
627
1027
 
@@ -8,6 +8,21 @@ export const CHAT_CARD_SELECTORS = Object.freeze([
8
8
  "div[role=\"listitem\"]"
9
9
  ]);
10
10
 
11
+ export const CHAT_LIST_CONTAINER_SELECTORS = Object.freeze([
12
+ ".chat-list",
13
+ ".chat-list-content",
14
+ ".chat-left",
15
+ ".chat-left-main",
16
+ ".chat-message-list-left",
17
+ ".chat-conversation-list",
18
+ ".geek-list",
19
+ ".geek-list-wrap",
20
+ ".chat-list-wrap",
21
+ ".user-list",
22
+ ".conversation-list",
23
+ "div[role=\"list\"]"
24
+ ]);
25
+
11
26
  export const CHAT_BOTTOM_MARKER_SELECTORS = Object.freeze([
12
27
  "div[role=\"tfoot\"] .load-tips",
13
28
  "p.load-tips",
@@ -22,7 +22,8 @@ import {
22
22
  detectInfiniteListBottomMarker,
23
23
  getNextInfiniteListCandidate,
24
24
  markInfiniteListCandidateProcessed,
25
- resetInfiniteListForRefreshRound
25
+ resetInfiniteListForRefreshRound,
26
+ resolveInfiniteListFallbackPoint
26
27
  } from "../../core/infinite-list/index.js";
27
28
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
28
29
  import { createRunLifecycleManager } from "../../core/run/index.js";
@@ -38,6 +39,8 @@ import {
38
39
  } from "../../core/screening/index.js";
39
40
  import {
40
41
  CHAT_BOTTOM_MARKER_SELECTORS,
42
+ CHAT_CARD_SELECTORS,
43
+ CHAT_LIST_CONTAINER_SELECTORS,
41
44
  CHAT_TARGET_URL
42
45
  } from "./constants.js";
43
46
  import {
@@ -682,6 +685,14 @@ export async function runChatWorkflow({
682
685
  const results = [];
683
686
  let cardNodeIds = [];
684
687
  let listEndReason = "";
688
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
689
+ rootNodeId: rootState?.rootNodes?.top,
690
+ containerSelectors: CHAT_LIST_CONTAINER_SELECTORS,
691
+ itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
692
+ itemSelectors: CHAT_CARD_SELECTORS,
693
+ viewportPoint: { xRatio: 0.16, yRatio: 0.4 },
694
+ validateViewportPoint: true
695
+ }));
685
696
  let requestedCount = 0;
686
697
  let requestSatisfiedCount = 0;
687
698
  let requestSkippedCount = 0;
@@ -921,7 +932,7 @@ export async function runChatWorkflow({
921
932
  stableSignatureLimit: listStableSignatureLimit,
922
933
  wheelDeltaY: listWheelDeltaY,
923
934
  settleMs: listSettleMs,
924
- fallbackPoint: listFallbackPoint,
935
+ fallbackPoint: listFallbackResolver,
925
936
  findNodeIds: async () => {
926
937
  const currentRootState = await ensureChatViewport(await getChatRoots(client), "candidate_find_nodes");
927
938
  rootState = currentRootState;
@@ -58,6 +58,19 @@ export const RECOMMEND_CARD_SELECTOR = [
58
58
  "a[data-geekid]"
59
59
  ].join(", ");
60
60
 
61
+ export const RECOMMEND_LIST_CONTAINER_SELECTORS = Object.freeze([
62
+ ".recommend-list",
63
+ ".recommend-list-wrap",
64
+ ".candidate-list",
65
+ ".candidate-card-list",
66
+ ".candidate-card-wrap-list",
67
+ ".geek-list",
68
+ ".geek-list-wrap",
69
+ ".card-list",
70
+ ".list-wrap",
71
+ ".content-list"
72
+ ]);
73
+
61
74
  export const RECOMMEND_END_REFRESH_SELECTOR = [
62
75
  ".btn",
63
76
  "button",
@@ -24,7 +24,8 @@ import {
24
24
  detectInfiniteListBottomMarker,
25
25
  getNextInfiniteListCandidate,
26
26
  markInfiniteListCandidateProcessed,
27
- resetInfiniteListForRefreshRound
27
+ resetInfiniteListForRefreshRound,
28
+ resolveInfiniteListFallbackPoint
28
29
  } from "../../core/infinite-list/index.js";
29
30
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
30
31
  import {
@@ -57,7 +58,9 @@ import {
57
58
  } from "./scopes.js";
58
59
  import {
59
60
  RECOMMEND_BOTTOM_MARKER_SELECTORS,
60
- RECOMMEND_END_REFRESH_SELECTOR
61
+ RECOMMEND_CARD_SELECTOR,
62
+ RECOMMEND_END_REFRESH_SELECTOR,
63
+ RECOMMEND_LIST_CONTAINER_SELECTORS
61
64
  } from "./constants.js";
62
65
  import {
63
66
  clickRecommendActionControl,
@@ -451,6 +454,14 @@ export async function runRecommendWorkflow({
451
454
  let filterResult = null;
452
455
  let cardNodeIds = [];
453
456
  let listEndReason = "";
457
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
458
+ rootNodeId: rootState?.iframe?.documentNodeId,
459
+ containerSelectors: RECOMMEND_LIST_CONTAINER_SELECTORS,
460
+ itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
461
+ itemSelectors: [RECOMMEND_CARD_SELECTOR],
462
+ viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
463
+ validateViewportPoint: true
464
+ }));
454
465
 
455
466
  runControl.setPhase("recommend:cleanup");
456
467
  await closeRecommendDetail(client, { attemptsLimit: 2 });
@@ -567,7 +578,7 @@ export async function runRecommendWorkflow({
567
578
  stableSignatureLimit: listStableSignatureLimit,
568
579
  wheelDeltaY: listWheelDeltaY,
569
580
  settleMs: listSettleMs,
570
- fallbackPoint: listFallbackPoint,
581
+ fallbackPoint: listFallbackResolver,
571
582
  findNodeIds: async () => {
572
583
  let currentRootState = await getRecommendRoots(client);
573
584
  currentRootState = await ensureRecommendViewport(currentRootState, "candidate_find_nodes");
@@ -16,6 +16,18 @@ export const RECRUIT_CARD_SELECTOR = [
16
16
  "a[data-geekid]"
17
17
  ].join(", ");
18
18
 
19
+ export const RECRUIT_LIST_CONTAINER_SELECTORS = Object.freeze([
20
+ ".search-list",
21
+ ".search-result-list",
22
+ ".candidate-list",
23
+ ".geek-list",
24
+ ".geek-list-wrap",
25
+ ".card-list",
26
+ ".list-wrap",
27
+ ".search-content",
28
+ ".search-container"
29
+ ]);
30
+
19
31
  export const RECRUIT_NO_DATA_SELECTORS = Object.freeze([
20
32
  "i.tip-nodata",
21
33
  ".tip-nodata",
@@ -26,12 +38,14 @@ export const RECRUIT_NO_DATA_SELECTORS = Object.freeze([
26
38
 
27
39
  export const RECRUIT_BOTTOM_MARKER_SELECTORS = Object.freeze([
28
40
  ".finished-wrap",
41
+ ".loadmore",
29
42
  ".load-tips",
30
43
  ".tip-nodata",
31
44
  ".empty-tip",
32
45
  ".empty-text",
33
46
  ".no-data",
34
47
  "[class*=\"finished\"]",
48
+ "[class*=\"loadmore\"]",
35
49
  "[class*=\"load-tips\"]",
36
50
  "[class*=\"empty\"]"
37
51
  ]);
@@ -22,7 +22,8 @@ import {
22
22
  detectInfiniteListBottomMarker,
23
23
  getNextInfiniteListCandidate,
24
24
  markInfiniteListCandidateProcessed,
25
- resetInfiniteListForRefreshRound
25
+ resetInfiniteListForRefreshRound,
26
+ resolveInfiniteListFallbackPoint
26
27
  } from "../../core/infinite-list/index.js";
27
28
  import { createViewportRunGuard } from "../../core/self-heal/index.js";
28
29
  import {
@@ -52,7 +53,9 @@ import { refreshRecruitSearchAtEnd } from "./refresh.js";
52
53
  import { getRecruitRoots } from "./roots.js";
53
54
  import {
54
55
  RECRUIT_BOTTOM_MARKER_SELECTORS,
55
- RECRUIT_BOTTOM_REFRESH_SELECTORS
56
+ RECRUIT_BOTTOM_REFRESH_SELECTORS,
57
+ RECRUIT_CARD_SELECTOR,
58
+ RECRUIT_LIST_CONTAINER_SELECTORS
56
59
  } from "./constants.js";
57
60
 
58
61
  function compactScreening(screening) {
@@ -194,6 +197,14 @@ export async function runRecruitWorkflow({
194
197
  let refreshRounds = 0;
195
198
  let cardNodeIds = [];
196
199
  let listEndReason = "";
200
+ const listFallbackResolver = listFallbackPoint || (async ({ items = [] } = {}) => resolveInfiniteListFallbackPoint(client, {
201
+ rootNodeId: rootState?.iframe?.documentNodeId,
202
+ containerSelectors: RECRUIT_LIST_CONTAINER_SELECTORS,
203
+ itemNodeIds: items.map((item) => item.node_id).filter(Boolean),
204
+ itemSelectors: [RECRUIT_CARD_SELECTOR],
205
+ viewportPoint: { xRatio: 0.28, yRatio: 0.5 },
206
+ validateViewportPoint: true
207
+ }));
197
208
 
198
209
  runControl.setPhase("recruit:cleanup");
199
210
  await closeRecruitDetail(client, { attemptsLimit: 2 });
@@ -283,7 +294,7 @@ export async function runRecruitWorkflow({
283
294
  stableSignatureLimit: listStableSignatureLimit,
284
295
  wheelDeltaY: listWheelDeltaY,
285
296
  settleMs: listSettleMs,
286
- fallbackPoint: listFallbackPoint,
297
+ fallbackPoint: listFallbackResolver,
287
298
  findNodeIds: async () => {
288
299
  let currentRootState = await getRecruitRoots(client);
289
300
  currentRootState = await ensureRecruitViewport(currentRootState, "candidate_find_nodes");