@reconcrap/boss-recommend-mcp 2.0.24 → 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 +1 -1
- package/src/core/infinite-list/index.js +408 -8
- package/src/domains/chat/constants.js +15 -0
- package/src/domains/chat/detail.js +43 -1
- package/src/domains/chat/run-service.js +13 -2
- package/src/domains/recommend/constants.js +13 -0
- package/src/domains/recommend/run-service.js +14 -3
- package/src/domains/recruit/constants.js +14 -0
- package/src/domains/recruit/run-service.js +14 -3
package/package.json
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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:
|
|
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:
|
|
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",
|
|
@@ -1139,6 +1139,7 @@ export async function clickChatAskResume(client, {
|
|
|
1139
1139
|
} = {}) {
|
|
1140
1140
|
const started = Date.now();
|
|
1141
1141
|
let lastState = null;
|
|
1142
|
+
let lastDisabledAskResume = null;
|
|
1142
1143
|
while (Date.now() - started <= timeoutMs) {
|
|
1143
1144
|
const state = await readChatConversationReadyState(client);
|
|
1144
1145
|
lastState = state;
|
|
@@ -1172,6 +1173,9 @@ export async function clickChatAskResume(client, {
|
|
|
1172
1173
|
};
|
|
1173
1174
|
}
|
|
1174
1175
|
}
|
|
1176
|
+
if (state.ask_resume?.node_id && state.ask_resume.disabled) {
|
|
1177
|
+
lastDisabledAskResume = state.ask_resume;
|
|
1178
|
+
}
|
|
1175
1179
|
if (state.already_requested_resume) {
|
|
1176
1180
|
return {
|
|
1177
1181
|
ok: true,
|
|
@@ -1181,6 +1185,16 @@ export async function clickChatAskResume(client, {
|
|
|
1181
1185
|
}
|
|
1182
1186
|
await sleep(250);
|
|
1183
1187
|
}
|
|
1188
|
+
if (lastDisabledAskResume) {
|
|
1189
|
+
return {
|
|
1190
|
+
ok: false,
|
|
1191
|
+
already_requested: true,
|
|
1192
|
+
request_pending: true,
|
|
1193
|
+
error: "ASK_RESUME_BUTTON_DISABLED",
|
|
1194
|
+
control: lastDisabledAskResume,
|
|
1195
|
+
state: lastState
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1184
1198
|
return {
|
|
1185
1199
|
ok: false,
|
|
1186
1200
|
error: "ASK_RESUME_BUTTON_NOT_FOUND",
|
|
@@ -1317,6 +1331,7 @@ export async function waitForChatResumeRequestMessage(client, {
|
|
|
1317
1331
|
export async function requestChatResumeForPassedCandidate(client, {
|
|
1318
1332
|
greetingText = "Hi同学,能麻烦发下简历吗?",
|
|
1319
1333
|
maxAttempts = 3,
|
|
1334
|
+
askResumeTimeoutMs = 8000,
|
|
1320
1335
|
dryRun = false
|
|
1321
1336
|
} = {}) {
|
|
1322
1337
|
const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
|
|
@@ -1362,13 +1377,17 @@ export async function requestChatResumeForPassedCandidate(client, {
|
|
|
1362
1377
|
const attempts = [];
|
|
1363
1378
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1364
1379
|
const before = await getChatResumeRequestMessageState(client);
|
|
1365
|
-
const askResult = await clickChatAskResume(client
|
|
1380
|
+
const askResult = await clickChatAskResume(client, {
|
|
1381
|
+
timeoutMs: askResumeTimeoutMs
|
|
1382
|
+
});
|
|
1366
1383
|
let confirmResult = {
|
|
1367
1384
|
confirmed: false,
|
|
1368
1385
|
assumed_requested: Boolean(askResult.already_requested),
|
|
1369
1386
|
skipped: true,
|
|
1370
1387
|
reason: askResult.attachment_resume_available
|
|
1371
1388
|
? "attachment_resume_already_available"
|
|
1389
|
+
: askResult.request_pending
|
|
1390
|
+
? "resume_request_already_pending"
|
|
1372
1391
|
: askResult.ok
|
|
1373
1392
|
? "already_requested"
|
|
1374
1393
|
: (askResult.error || "ask_resume_not_clicked")
|
|
@@ -1394,6 +1413,29 @@ export async function requestChatResumeForPassedCandidate(client, {
|
|
|
1394
1413
|
attempts
|
|
1395
1414
|
};
|
|
1396
1415
|
}
|
|
1416
|
+
if (askResult.request_pending || askResult.already_requested) {
|
|
1417
|
+
attempts.push({
|
|
1418
|
+
attempt: attempt + 1,
|
|
1419
|
+
ask_result: askResult,
|
|
1420
|
+
confirm_result: confirmResult,
|
|
1421
|
+
message_before_count: before.count,
|
|
1422
|
+
message_after_count: before.count,
|
|
1423
|
+
resume_attachment_before_count: before.resume_attachment_count || 0,
|
|
1424
|
+
resume_attachment_after_count: before.resume_attachment_count || 0,
|
|
1425
|
+
message_observed: false,
|
|
1426
|
+
message_last_text: before.last_success_text || before.last_text || ""
|
|
1427
|
+
});
|
|
1428
|
+
return {
|
|
1429
|
+
requested: false,
|
|
1430
|
+
skipped: true,
|
|
1431
|
+
reason: "resume_request_already_pending",
|
|
1432
|
+
initial_state: initialState,
|
|
1433
|
+
close_before_greeting: closeBeforeGreeting,
|
|
1434
|
+
greeting_sent: true,
|
|
1435
|
+
greeting_send_result: sendResult,
|
|
1436
|
+
attempts
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1397
1439
|
if (askResult.ok && !askResult.already_requested) {
|
|
1398
1440
|
confirmResult = await clickChatConfirmRequestResume(client);
|
|
1399
1441
|
}
|
|
@@ -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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
297
|
+
fallbackPoint: listFallbackResolver,
|
|
287
298
|
findNodeIds: async () => {
|
|
288
299
|
let currentRootState = await getRecruitRoots(client);
|
|
289
300
|
currentRootState = await ensureRecruitViewport(currentRootState, "candidate_find_nodes");
|