@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +8 -8
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2419 -2415
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +650 -650
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,564 +1,564 @@
1
- import {
2
- getNodeBox,
3
- querySelector,
4
- sleep
5
- } from "../browser/index.js";
6
-
7
- export const VIEWPORT_COLLAPSE_RATIO_THRESHOLD = 0.6;
8
- export const VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH = 1000;
9
- export const VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO = 0.85;
10
-
11
- const ABSOLUTE_COLLAPSE_LIMITS = Object.freeze({
12
- clientHeight: 260,
13
- clientWidth: 280,
14
- frameHeight: 320,
15
- frameWidth: 460,
16
- viewportHeight: 260,
17
- viewportWidth: 360
18
- });
19
-
20
- function normalizeText(value) {
21
- return String(value ?? "").replace(/\s+/g, " ").trim();
22
- }
23
-
24
- function getPositiveNumber(...values) {
25
- for (const value of values) {
26
- const number = Number(value);
27
- if (Number.isFinite(number) && number > 0) return number;
28
- }
29
- return 0;
30
- }
31
-
32
- function rootNodeId(roots = {}, name) {
33
- const root = roots[name];
34
- if (typeof root === "number") return root;
35
- if (root?.nodeId) return root.nodeId;
36
- if (root?.documentNodeId) return root.documentNodeId;
37
- return 0;
38
- }
39
-
40
- function compactRect(rect = {}) {
41
- return {
42
- width: getPositiveNumber(rect.width),
43
- height: getPositiveNumber(rect.height)
44
- };
45
- }
46
-
47
- function pickViewportSize(layoutMetrics = {}, axis = "width") {
48
- const clientKey = axis === "width" ? "clientWidth" : "clientHeight";
49
- return getPositiveNumber(
50
- layoutMetrics?.cssVisualViewport?.[clientKey],
51
- layoutMetrics?.cssLayoutViewport?.[clientKey],
52
- layoutMetrics?.visualViewport?.[clientKey],
53
- layoutMetrics?.layoutViewport?.[clientKey]
54
- );
55
- }
56
-
57
- async function getLayoutMetrics(client) {
58
- if (typeof client?.Page?.getLayoutMetrics !== "function") return null;
59
- try {
60
- return await client.Page.getLayoutMetrics();
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- export async function getCurrentWindowInfo(client) {
67
- if (typeof client?.Browser?.getWindowForTarget !== "function") {
68
- return {
69
- ok: false,
70
- unsupported: true,
71
- error: "Browser.getWindowForTarget is not available"
72
- };
73
- }
74
-
75
- try {
76
- const targetWindow = await client.Browser.getWindowForTarget({});
77
- let bounds = targetWindow?.bounds || null;
78
- if (
79
- targetWindow?.windowId
80
- && typeof client?.Browser?.getWindowBounds === "function"
81
- ) {
82
- const currentBounds = await client.Browser.getWindowBounds({
83
- windowId: targetWindow.windowId
84
- });
85
- bounds = currentBounds?.bounds || bounds;
86
- }
87
- return {
88
- ok: true,
89
- windowId: targetWindow?.windowId || null,
90
- bounds
91
- };
92
- } catch (error) {
93
- return {
94
- ok: false,
95
- error: error?.message || String(error)
96
- };
97
- }
98
- }
99
-
100
- async function readBox(client, nodeId) {
101
- if (!nodeId) return null;
102
- try {
103
- return await getNodeBox(client, nodeId);
104
- } catch {
105
- return null;
106
- }
107
- }
108
-
109
- async function readBestContentBox(client, rootNodeIdValue) {
110
- const directBox = await readBox(client, rootNodeIdValue);
111
- if (directBox?.rect?.width && directBox?.rect?.height) return directBox;
112
-
113
- for (const selector of ["body", "html"]) {
114
- const nodeId = await querySelector(client, rootNodeIdValue, selector).catch(() => 0);
115
- const box = await readBox(client, nodeId);
116
- if (box?.rect?.width && box?.rect?.height) return box;
117
- }
118
- return directBox;
119
- }
120
-
121
- export function buildViewportHealthDiagnostics(state, windowInfo = null, layoutMetrics = null) {
122
- const topViewport = state?.topViewport || {};
123
- const bounds = windowInfo?.bounds || null;
124
- const windowState = normalizeText(bounds?.windowState || "").toLowerCase() || null;
125
- const windowWidth = getPositiveNumber(bounds?.width);
126
- const screenAvailWidth = getPositiveNumber(topViewport.screenAvailWidth);
127
- const topOuterWidth = getPositiveNumber(topViewport.outerWidth);
128
- const actualWidth = getPositiveNumber(
129
- layoutMetrics?.cssVisualViewport?.clientWidth,
130
- layoutMetrics?.cssLayoutViewport?.clientWidth,
131
- topViewport.visualWidth,
132
- topViewport.innerWidth,
133
- state?.viewport?.width,
134
- state?.clientWidth,
135
- state?.frameRect?.width
136
- );
137
- const actualHeight = getPositiveNumber(
138
- layoutMetrics?.cssVisualViewport?.clientHeight,
139
- layoutMetrics?.cssLayoutViewport?.clientHeight,
140
- topViewport.visualHeight,
141
- topViewport.innerHeight,
142
- state?.viewport?.height,
143
- state?.clientHeight,
144
- state?.frameRect?.height
145
- );
146
- const hasScreenWidth = screenAvailWidth > 0;
147
- const nearFullscreen = Boolean(
148
- windowState === "maximized"
149
- || (
150
- windowWidth > 0
151
- && hasScreenWidth
152
- && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
153
- )
154
- || (
155
- topOuterWidth > 0
156
- && hasScreenWidth
157
- && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
158
- )
159
- );
160
- const fallbackExpectedWidth = getPositiveNumber(screenAvailWidth, windowWidth, topOuterWidth);
161
- let expectedWidth = 0;
162
- if (windowWidth > 0) {
163
- expectedWidth = hasScreenWidth && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
164
- ? Math.min(windowWidth, screenAvailWidth)
165
- : windowWidth;
166
- } else if (topOuterWidth > 0) {
167
- expectedWidth = hasScreenWidth && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
168
- ? Math.min(topOuterWidth, screenAvailWidth)
169
- : topOuterWidth;
170
- } else {
171
- expectedWidth = fallbackExpectedWidth;
172
- }
173
- const widthRatio = actualWidth > 0 && expectedWidth > 0
174
- ? actualWidth / expectedWidth
175
- : null;
176
- const relativeCollapsed = Boolean(
177
- nearFullscreen
178
- && expectedWidth >= VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH
179
- && actualWidth > 0
180
- && widthRatio !== null
181
- && widthRatio <= VIEWPORT_COLLAPSE_RATIO_THRESHOLD
182
- );
183
-
184
- return {
185
- threshold: VIEWPORT_COLLAPSE_RATIO_THRESHOLD,
186
- minExpectedWidth: VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
187
- nearFullscreen,
188
- windowState,
189
- windowWidth,
190
- screenAvailWidth,
191
- topOuterWidth,
192
- actualWidth,
193
- actualHeight,
194
- expectedWidth,
195
- widthRatio,
196
- relativeCollapsed
197
- };
198
- }
199
-
200
- export function isListViewportCollapsed(state) {
201
- if (!state?.ok) return false;
202
- if (state.viewportDiagnostics?.relativeCollapsed === true) return true;
203
- const clientHeight = Number(state.clientHeight || 0);
204
- const clientWidth = Number(state.clientWidth || 0);
205
- const frameWidth = Number(state.frameRect?.width || 0);
206
- const frameHeight = Number(state.frameRect?.height || 0);
207
- const viewportWidth = Number(state.viewport?.width || 0);
208
- const viewportHeight = Number(state.viewport?.height || 0);
209
-
210
- return (
211
- (clientHeight > 0 && clientHeight < ABSOLUTE_COLLAPSE_LIMITS.clientHeight)
212
- || (clientWidth > 0 && clientWidth < ABSOLUTE_COLLAPSE_LIMITS.clientWidth)
213
- || (frameHeight > 0 && frameHeight < ABSOLUTE_COLLAPSE_LIMITS.frameHeight)
214
- || (frameWidth > 0 && frameWidth < ABSOLUTE_COLLAPSE_LIMITS.frameWidth)
215
- || (viewportHeight > 0 && viewportHeight < ABSOLUTE_COLLAPSE_LIMITS.viewportHeight)
216
- || (viewportWidth > 0 && viewportWidth < ABSOLUTE_COLLAPSE_LIMITS.viewportWidth)
217
- );
218
- }
219
-
220
- export async function readViewportState(client, {
221
- roots = {},
222
- root = "frame",
223
- frameOwnerRoot = "frameOwner"
224
- } = {}) {
225
- const targetRootNodeId = rootNodeId(roots, root);
226
- if (!targetRootNodeId) {
227
- return {
228
- ok: false,
229
- root,
230
- error: `Root not found: ${root}`
231
- };
232
- }
233
-
234
- const layoutMetrics = await getLayoutMetrics(client);
235
- const windowInfo = await getCurrentWindowInfo(client);
236
- const contentBox = await readBestContentBox(client, targetRootNodeId);
237
- const ownerNodeId = rootNodeId(roots, frameOwnerRoot);
238
- const ownerBox = ownerNodeId ? await readBox(client, ownerNodeId) : null;
239
- const frameRect = compactRect(ownerBox?.rect || contentBox?.rect || {});
240
- const clientWidth = getPositiveNumber(
241
- contentBox?.rect?.width,
242
- frameRect.width,
243
- pickViewportSize(layoutMetrics, "width")
244
- );
245
- const clientHeight = getPositiveNumber(
246
- contentBox?.rect?.height,
247
- frameRect.height,
248
- pickViewportSize(layoutMetrics, "height")
249
- );
250
- const viewportWidth = pickViewportSize(layoutMetrics, "width") || clientWidth;
251
- const viewportHeight = pickViewportSize(layoutMetrics, "height") || clientHeight;
252
- const bounds = windowInfo?.bounds || {};
253
- const topViewport = {
254
- innerWidth: viewportWidth,
255
- innerHeight: viewportHeight,
256
- outerWidth: getPositiveNumber(bounds.width, viewportWidth),
257
- outerHeight: getPositiveNumber(bounds.height, viewportHeight),
258
- visualWidth: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientWidth, viewportWidth),
259
- visualHeight: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientHeight, viewportHeight),
260
- screenAvailWidth: getPositiveNumber(bounds.width),
261
- screenAvailHeight: getPositiveNumber(bounds.height),
262
- devicePixelRatio: getPositiveNumber(layoutMetrics?.cssVisualViewport?.scale, 1)
263
- };
264
- const state = {
265
- ok: true,
266
- root,
267
- rootNodeId: targetRootNodeId,
268
- frameOwnerRoot,
269
- frameOwnerNodeId: ownerNodeId || null,
270
- clientWidth,
271
- clientHeight,
272
- frameRect,
273
- viewport: {
274
- width: viewportWidth,
275
- height: viewportHeight
276
- },
277
- topViewport,
278
- windowInfo
279
- };
280
- state.viewportDiagnostics = buildViewportHealthDiagnostics(state, windowInfo, layoutMetrics);
281
- state.collapsed = isListViewportCollapsed(state);
282
- return state;
283
- }
284
-
285
- export async function setWindowStateIfPossible(client, windowState, reason = "viewport_recovery") {
286
- const windowInfo = await getCurrentWindowInfo(client);
287
- if (!windowInfo.ok || !windowInfo.windowId || typeof client?.Browser?.setWindowBounds !== "function") {
288
- return {
289
- ok: false,
290
- reason,
291
- windowState,
292
- error: windowInfo.error || "Browser.setWindowBounds is not available"
293
- };
294
- }
295
-
296
- try {
297
- await client.Browser.setWindowBounds({
298
- windowId: windowInfo.windowId,
299
- bounds: {
300
- windowState
301
- }
302
- });
303
- return {
304
- ok: true,
305
- reason,
306
- windowState,
307
- windowId: windowInfo.windowId,
308
- before: windowInfo.bounds || null
309
- };
310
- } catch (error) {
311
- return {
312
- ok: false,
313
- reason,
314
- windowState,
315
- windowId: windowInfo.windowId,
316
- error: error?.message || String(error)
317
- };
318
- }
319
- }
320
-
321
- export async function toggleWindowStateForViewportRecovery(client, {
322
- reason = "viewport_recovery",
323
- settleMs = 520,
324
- bringToFront = true
325
- } = {}) {
326
- const currentInfo = await getCurrentWindowInfo(client);
327
- const currentState = normalizeText(currentInfo?.bounds?.windowState || "").toLowerCase();
328
- const sequence = currentState === "normal"
329
- ? ["maximized"]
330
- : ["normal", "maximized"];
331
- const attempts = [];
332
-
333
- for (const windowState of sequence) {
334
- const attempt = await setWindowStateIfPossible(client, windowState, reason);
335
- attempts.push(attempt);
336
- if (attempt.ok && settleMs > 0) await sleep(settleMs);
337
- }
338
-
339
- if (bringToFront && typeof client?.Page?.bringToFront === "function") {
340
- await client.Page.bringToFront();
341
- }
342
-
343
- return {
344
- ok: attempts.some((attempt) => attempt.ok),
345
- applied: attempts.some((attempt) => attempt.ok),
346
- reason,
347
- current_state: currentState || null,
348
- sequence,
349
- attempts
350
- };
351
- }
352
-
353
- export function compactViewportState(state = null) {
354
- if (!state) return null;
355
- return {
356
- ok: Boolean(state.ok),
357
- root: state.root || null,
358
- error: state.error || null,
359
- clientWidth: state.clientWidth || 0,
360
- clientHeight: state.clientHeight || 0,
361
- frameRect: state.frameRect || null,
362
- viewport: state.viewport || null,
363
- topViewport: state.topViewport
364
- ? {
365
- innerWidth: state.topViewport.innerWidth || 0,
366
- innerHeight: state.topViewport.innerHeight || 0,
367
- outerWidth: state.topViewport.outerWidth || 0,
368
- outerHeight: state.topViewport.outerHeight || 0,
369
- visualWidth: state.topViewport.visualWidth || 0,
370
- visualHeight: state.topViewport.visualHeight || 0,
371
- screenAvailWidth: state.topViewport.screenAvailWidth || 0,
372
- screenAvailHeight: state.topViewport.screenAvailHeight || 0,
373
- devicePixelRatio: state.topViewport.devicePixelRatio || 0
374
- }
375
- : null,
376
- viewportDiagnostics: state.viewportDiagnostics || null,
377
- collapsed: Boolean(state.collapsed)
378
- };
379
- }
380
-
381
- export function compactViewportHealthResult(result = null) {
382
- if (!result) return null;
383
- return {
384
- ok: Boolean(result.ok),
385
- collapsed: Boolean(result.collapsed),
386
- recovered: Boolean(result.recovered),
387
- reason: result.reason || null,
388
- state: compactViewportState(result.state),
389
- before: compactViewportState(result.before),
390
- repair: result.repair
391
- ? {
392
- ok: Boolean(result.repair.ok),
393
- applied: Boolean(result.repair.applied),
394
- current_state: result.repair.current_state || null,
395
- sequence: result.repair.sequence || [],
396
- attempts: (result.repair.attempts || []).map((attempt) => ({
397
- ok: Boolean(attempt.ok),
398
- windowState: attempt.windowState,
399
- error: attempt.error || null
400
- }))
401
- }
402
- : null,
403
- error: result.error || null
404
- };
405
- }
406
-
407
- export async function ensureHealthyViewport(client, {
408
- roots = {},
409
- root = "frame",
410
- frameOwnerRoot = "frameOwner",
411
- reason = "viewport_recovery",
412
- repair = true,
413
- recoveryDelayMs = 900
414
- } = {}) {
415
- const before = await readViewportState(client, {
416
- roots,
417
- root,
418
- frameOwnerRoot
419
- });
420
- if (!before.ok) {
421
- return {
422
- ok: false,
423
- collapsed: false,
424
- recovered: false,
425
- reason,
426
- state: before,
427
- error: before.error || "viewport state could not be read"
428
- };
429
- }
430
-
431
- if (!isListViewportCollapsed(before)) {
432
- return {
433
- ok: true,
434
- collapsed: false,
435
- recovered: false,
436
- reason,
437
- state: before
438
- };
439
- }
440
-
441
- if (!repair) {
442
- return {
443
- ok: false,
444
- collapsed: true,
445
- recovered: false,
446
- reason,
447
- before,
448
- state: before,
449
- error: "viewport collapsed and repair disabled"
450
- };
451
- }
452
-
453
- const repairResult = await toggleWindowStateForViewportRecovery(client, { reason });
454
- if (recoveryDelayMs > 0) await sleep(recoveryDelayMs);
455
- const after = await readViewportState(client, {
456
- roots,
457
- root,
458
- frameOwnerRoot
459
- });
460
- const stillCollapsed = isListViewportCollapsed(after);
461
- return {
462
- ok: after.ok && !stillCollapsed,
463
- collapsed: stillCollapsed,
464
- recovered: after.ok && !stillCollapsed && repairResult.applied,
465
- reason,
466
- before,
467
- state: after,
468
- repair: repairResult,
469
- error: after.ok && !stillCollapsed
470
- ? null
471
- : "viewport collapsed after recovery attempt"
472
- };
473
- }
474
-
475
- export function createViewportRunGuard({
476
- client,
477
- domain = "boss",
478
- root = "frame",
479
- frameOwnerRoot = "frameOwner",
480
- runControl = null,
481
- getRoots = null,
482
- rootNodesFromState = (rootState) => rootState?.rootNodes || rootState?.roots || rootState || {},
483
- repair = true,
484
- maxEvents = 10
485
- } = {}) {
486
- if (!client) throw new Error("createViewportRunGuard requires a guarded CDP client");
487
- const events = [];
488
- const stats = {
489
- checks: 0,
490
- recoveries: 0,
491
- failures: 0
492
- };
493
-
494
- function recordEvent(phase, health) {
495
- const compact = compactViewportHealthResult(health);
496
- const shouldRecord = Boolean(health?.recovered || !health?.ok || health?.collapsed);
497
- if (!shouldRecord) return compact;
498
- const event = {
499
- phase,
500
- at: new Date().toISOString(),
501
- ...compact
502
- };
503
- events.push(event);
504
- if (events.length > maxEvents) events.shift();
505
- if (runControl) {
506
- runControl.checkpoint({
507
- viewport_health: event,
508
- viewport_health_events: events.slice(),
509
- viewport_health_stats: { ...stats }
510
- });
511
- }
512
- return compact;
513
- }
514
-
515
- async function ensure(rootState, {
516
- phase = "run",
517
- reason = `${domain}:${phase}`
518
- } = {}) {
519
- let currentRootState = rootState;
520
- if (!currentRootState && typeof getRoots === "function") {
521
- currentRootState = await getRoots(client);
522
- }
523
- const roots = rootNodesFromState(currentRootState);
524
- stats.checks += 1;
525
- const health = await ensureHealthyViewport(client, {
526
- roots,
527
- root,
528
- frameOwnerRoot,
529
- reason,
530
- repair
531
- });
532
- if (health.recovered) stats.recoveries += 1;
533
- if (!health.ok) stats.failures += 1;
534
- const compact = recordEvent(phase, health);
535
- if (!health.ok) {
536
- const error = new Error(`${String(domain).toUpperCase()}_LIST_VIEWPORT_COLLAPSED`);
537
- error.code = "LIST_VIEWPORT_COLLAPSED";
538
- error.domain = domain;
539
- error.phase = phase;
540
- error.viewport_health = compact;
541
- throw error;
542
- }
543
- if (health.recovered && typeof getRoots === "function") {
544
- currentRootState = await getRoots(client);
545
- }
546
- return {
547
- rootState: currentRootState,
548
- health,
549
- compact,
550
- stats: { ...stats },
551
- events: events.slice()
552
- };
553
- }
554
-
555
- return {
556
- ensure,
557
- getStats() {
558
- return { ...stats };
559
- },
560
- getEvents() {
561
- return events.slice();
562
- }
563
- };
564
- }
1
+ import {
2
+ getNodeBox,
3
+ querySelector,
4
+ sleep
5
+ } from "../browser/index.js";
6
+
7
+ export const VIEWPORT_COLLAPSE_RATIO_THRESHOLD = 0.6;
8
+ export const VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH = 1000;
9
+ export const VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO = 0.85;
10
+
11
+ const ABSOLUTE_COLLAPSE_LIMITS = Object.freeze({
12
+ clientHeight: 260,
13
+ clientWidth: 280,
14
+ frameHeight: 320,
15
+ frameWidth: 460,
16
+ viewportHeight: 260,
17
+ viewportWidth: 360
18
+ });
19
+
20
+ function normalizeText(value) {
21
+ return String(value ?? "").replace(/\s+/g, " ").trim();
22
+ }
23
+
24
+ function getPositiveNumber(...values) {
25
+ for (const value of values) {
26
+ const number = Number(value);
27
+ if (Number.isFinite(number) && number > 0) return number;
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ function rootNodeId(roots = {}, name) {
33
+ const root = roots[name];
34
+ if (typeof root === "number") return root;
35
+ if (root?.nodeId) return root.nodeId;
36
+ if (root?.documentNodeId) return root.documentNodeId;
37
+ return 0;
38
+ }
39
+
40
+ function compactRect(rect = {}) {
41
+ return {
42
+ width: getPositiveNumber(rect.width),
43
+ height: getPositiveNumber(rect.height)
44
+ };
45
+ }
46
+
47
+ function pickViewportSize(layoutMetrics = {}, axis = "width") {
48
+ const clientKey = axis === "width" ? "clientWidth" : "clientHeight";
49
+ return getPositiveNumber(
50
+ layoutMetrics?.cssVisualViewport?.[clientKey],
51
+ layoutMetrics?.cssLayoutViewport?.[clientKey],
52
+ layoutMetrics?.visualViewport?.[clientKey],
53
+ layoutMetrics?.layoutViewport?.[clientKey]
54
+ );
55
+ }
56
+
57
+ async function getLayoutMetrics(client) {
58
+ if (typeof client?.Page?.getLayoutMetrics !== "function") return null;
59
+ try {
60
+ return await client.Page.getLayoutMetrics();
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ export async function getCurrentWindowInfo(client) {
67
+ if (typeof client?.Browser?.getWindowForTarget !== "function") {
68
+ return {
69
+ ok: false,
70
+ unsupported: true,
71
+ error: "Browser.getWindowForTarget is not available"
72
+ };
73
+ }
74
+
75
+ try {
76
+ const targetWindow = await client.Browser.getWindowForTarget({});
77
+ let bounds = targetWindow?.bounds || null;
78
+ if (
79
+ targetWindow?.windowId
80
+ && typeof client?.Browser?.getWindowBounds === "function"
81
+ ) {
82
+ const currentBounds = await client.Browser.getWindowBounds({
83
+ windowId: targetWindow.windowId
84
+ });
85
+ bounds = currentBounds?.bounds || bounds;
86
+ }
87
+ return {
88
+ ok: true,
89
+ windowId: targetWindow?.windowId || null,
90
+ bounds
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ ok: false,
95
+ error: error?.message || String(error)
96
+ };
97
+ }
98
+ }
99
+
100
+ async function readBox(client, nodeId) {
101
+ if (!nodeId) return null;
102
+ try {
103
+ return await getNodeBox(client, nodeId);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ async function readBestContentBox(client, rootNodeIdValue) {
110
+ const directBox = await readBox(client, rootNodeIdValue);
111
+ if (directBox?.rect?.width && directBox?.rect?.height) return directBox;
112
+
113
+ for (const selector of ["body", "html"]) {
114
+ const nodeId = await querySelector(client, rootNodeIdValue, selector).catch(() => 0);
115
+ const box = await readBox(client, nodeId);
116
+ if (box?.rect?.width && box?.rect?.height) return box;
117
+ }
118
+ return directBox;
119
+ }
120
+
121
+ export function buildViewportHealthDiagnostics(state, windowInfo = null, layoutMetrics = null) {
122
+ const topViewport = state?.topViewport || {};
123
+ const bounds = windowInfo?.bounds || null;
124
+ const windowState = normalizeText(bounds?.windowState || "").toLowerCase() || null;
125
+ const windowWidth = getPositiveNumber(bounds?.width);
126
+ const screenAvailWidth = getPositiveNumber(topViewport.screenAvailWidth);
127
+ const topOuterWidth = getPositiveNumber(topViewport.outerWidth);
128
+ const actualWidth = getPositiveNumber(
129
+ layoutMetrics?.cssVisualViewport?.clientWidth,
130
+ layoutMetrics?.cssLayoutViewport?.clientWidth,
131
+ topViewport.visualWidth,
132
+ topViewport.innerWidth,
133
+ state?.viewport?.width,
134
+ state?.clientWidth,
135
+ state?.frameRect?.width
136
+ );
137
+ const actualHeight = getPositiveNumber(
138
+ layoutMetrics?.cssVisualViewport?.clientHeight,
139
+ layoutMetrics?.cssLayoutViewport?.clientHeight,
140
+ topViewport.visualHeight,
141
+ topViewport.innerHeight,
142
+ state?.viewport?.height,
143
+ state?.clientHeight,
144
+ state?.frameRect?.height
145
+ );
146
+ const hasScreenWidth = screenAvailWidth > 0;
147
+ const nearFullscreen = Boolean(
148
+ windowState === "maximized"
149
+ || (
150
+ windowWidth > 0
151
+ && hasScreenWidth
152
+ && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
153
+ )
154
+ || (
155
+ topOuterWidth > 0
156
+ && hasScreenWidth
157
+ && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
158
+ )
159
+ );
160
+ const fallbackExpectedWidth = getPositiveNumber(screenAvailWidth, windowWidth, topOuterWidth);
161
+ let expectedWidth = 0;
162
+ if (windowWidth > 0) {
163
+ expectedWidth = hasScreenWidth && windowWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
164
+ ? Math.min(windowWidth, screenAvailWidth)
165
+ : windowWidth;
166
+ } else if (topOuterWidth > 0) {
167
+ expectedWidth = hasScreenWidth && topOuterWidth >= screenAvailWidth * VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO
168
+ ? Math.min(topOuterWidth, screenAvailWidth)
169
+ : topOuterWidth;
170
+ } else {
171
+ expectedWidth = fallbackExpectedWidth;
172
+ }
173
+ const widthRatio = actualWidth > 0 && expectedWidth > 0
174
+ ? actualWidth / expectedWidth
175
+ : null;
176
+ const relativeCollapsed = Boolean(
177
+ nearFullscreen
178
+ && expectedWidth >= VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH
179
+ && actualWidth > 0
180
+ && widthRatio !== null
181
+ && widthRatio <= VIEWPORT_COLLAPSE_RATIO_THRESHOLD
182
+ );
183
+
184
+ return {
185
+ threshold: VIEWPORT_COLLAPSE_RATIO_THRESHOLD,
186
+ minExpectedWidth: VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
187
+ nearFullscreen,
188
+ windowState,
189
+ windowWidth,
190
+ screenAvailWidth,
191
+ topOuterWidth,
192
+ actualWidth,
193
+ actualHeight,
194
+ expectedWidth,
195
+ widthRatio,
196
+ relativeCollapsed
197
+ };
198
+ }
199
+
200
+ export function isListViewportCollapsed(state) {
201
+ if (!state?.ok) return false;
202
+ if (state.viewportDiagnostics?.relativeCollapsed === true) return true;
203
+ const clientHeight = Number(state.clientHeight || 0);
204
+ const clientWidth = Number(state.clientWidth || 0);
205
+ const frameWidth = Number(state.frameRect?.width || 0);
206
+ const frameHeight = Number(state.frameRect?.height || 0);
207
+ const viewportWidth = Number(state.viewport?.width || 0);
208
+ const viewportHeight = Number(state.viewport?.height || 0);
209
+
210
+ return (
211
+ (clientHeight > 0 && clientHeight < ABSOLUTE_COLLAPSE_LIMITS.clientHeight)
212
+ || (clientWidth > 0 && clientWidth < ABSOLUTE_COLLAPSE_LIMITS.clientWidth)
213
+ || (frameHeight > 0 && frameHeight < ABSOLUTE_COLLAPSE_LIMITS.frameHeight)
214
+ || (frameWidth > 0 && frameWidth < ABSOLUTE_COLLAPSE_LIMITS.frameWidth)
215
+ || (viewportHeight > 0 && viewportHeight < ABSOLUTE_COLLAPSE_LIMITS.viewportHeight)
216
+ || (viewportWidth > 0 && viewportWidth < ABSOLUTE_COLLAPSE_LIMITS.viewportWidth)
217
+ );
218
+ }
219
+
220
+ export async function readViewportState(client, {
221
+ roots = {},
222
+ root = "frame",
223
+ frameOwnerRoot = "frameOwner"
224
+ } = {}) {
225
+ const targetRootNodeId = rootNodeId(roots, root);
226
+ if (!targetRootNodeId) {
227
+ return {
228
+ ok: false,
229
+ root,
230
+ error: `Root not found: ${root}`
231
+ };
232
+ }
233
+
234
+ const layoutMetrics = await getLayoutMetrics(client);
235
+ const windowInfo = await getCurrentWindowInfo(client);
236
+ const contentBox = await readBestContentBox(client, targetRootNodeId);
237
+ const ownerNodeId = rootNodeId(roots, frameOwnerRoot);
238
+ const ownerBox = ownerNodeId ? await readBox(client, ownerNodeId) : null;
239
+ const frameRect = compactRect(ownerBox?.rect || contentBox?.rect || {});
240
+ const clientWidth = getPositiveNumber(
241
+ contentBox?.rect?.width,
242
+ frameRect.width,
243
+ pickViewportSize(layoutMetrics, "width")
244
+ );
245
+ const clientHeight = getPositiveNumber(
246
+ contentBox?.rect?.height,
247
+ frameRect.height,
248
+ pickViewportSize(layoutMetrics, "height")
249
+ );
250
+ const viewportWidth = pickViewportSize(layoutMetrics, "width") || clientWidth;
251
+ const viewportHeight = pickViewportSize(layoutMetrics, "height") || clientHeight;
252
+ const bounds = windowInfo?.bounds || {};
253
+ const topViewport = {
254
+ innerWidth: viewportWidth,
255
+ innerHeight: viewportHeight,
256
+ outerWidth: getPositiveNumber(bounds.width, viewportWidth),
257
+ outerHeight: getPositiveNumber(bounds.height, viewportHeight),
258
+ visualWidth: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientWidth, viewportWidth),
259
+ visualHeight: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientHeight, viewportHeight),
260
+ screenAvailWidth: getPositiveNumber(bounds.width),
261
+ screenAvailHeight: getPositiveNumber(bounds.height),
262
+ devicePixelRatio: getPositiveNumber(layoutMetrics?.cssVisualViewport?.scale, 1)
263
+ };
264
+ const state = {
265
+ ok: true,
266
+ root,
267
+ rootNodeId: targetRootNodeId,
268
+ frameOwnerRoot,
269
+ frameOwnerNodeId: ownerNodeId || null,
270
+ clientWidth,
271
+ clientHeight,
272
+ frameRect,
273
+ viewport: {
274
+ width: viewportWidth,
275
+ height: viewportHeight
276
+ },
277
+ topViewport,
278
+ windowInfo
279
+ };
280
+ state.viewportDiagnostics = buildViewportHealthDiagnostics(state, windowInfo, layoutMetrics);
281
+ state.collapsed = isListViewportCollapsed(state);
282
+ return state;
283
+ }
284
+
285
+ export async function setWindowStateIfPossible(client, windowState, reason = "viewport_recovery") {
286
+ const windowInfo = await getCurrentWindowInfo(client);
287
+ if (!windowInfo.ok || !windowInfo.windowId || typeof client?.Browser?.setWindowBounds !== "function") {
288
+ return {
289
+ ok: false,
290
+ reason,
291
+ windowState,
292
+ error: windowInfo.error || "Browser.setWindowBounds is not available"
293
+ };
294
+ }
295
+
296
+ try {
297
+ await client.Browser.setWindowBounds({
298
+ windowId: windowInfo.windowId,
299
+ bounds: {
300
+ windowState
301
+ }
302
+ });
303
+ return {
304
+ ok: true,
305
+ reason,
306
+ windowState,
307
+ windowId: windowInfo.windowId,
308
+ before: windowInfo.bounds || null
309
+ };
310
+ } catch (error) {
311
+ return {
312
+ ok: false,
313
+ reason,
314
+ windowState,
315
+ windowId: windowInfo.windowId,
316
+ error: error?.message || String(error)
317
+ };
318
+ }
319
+ }
320
+
321
+ export async function toggleWindowStateForViewportRecovery(client, {
322
+ reason = "viewport_recovery",
323
+ settleMs = 520,
324
+ bringToFront = true
325
+ } = {}) {
326
+ const currentInfo = await getCurrentWindowInfo(client);
327
+ const currentState = normalizeText(currentInfo?.bounds?.windowState || "").toLowerCase();
328
+ const sequence = currentState === "normal"
329
+ ? ["maximized"]
330
+ : ["normal", "maximized"];
331
+ const attempts = [];
332
+
333
+ for (const windowState of sequence) {
334
+ const attempt = await setWindowStateIfPossible(client, windowState, reason);
335
+ attempts.push(attempt);
336
+ if (attempt.ok && settleMs > 0) await sleep(settleMs);
337
+ }
338
+
339
+ if (bringToFront && typeof client?.Page?.bringToFront === "function") {
340
+ await client.Page.bringToFront();
341
+ }
342
+
343
+ return {
344
+ ok: attempts.some((attempt) => attempt.ok),
345
+ applied: attempts.some((attempt) => attempt.ok),
346
+ reason,
347
+ current_state: currentState || null,
348
+ sequence,
349
+ attempts
350
+ };
351
+ }
352
+
353
+ export function compactViewportState(state = null) {
354
+ if (!state) return null;
355
+ return {
356
+ ok: Boolean(state.ok),
357
+ root: state.root || null,
358
+ error: state.error || null,
359
+ clientWidth: state.clientWidth || 0,
360
+ clientHeight: state.clientHeight || 0,
361
+ frameRect: state.frameRect || null,
362
+ viewport: state.viewport || null,
363
+ topViewport: state.topViewport
364
+ ? {
365
+ innerWidth: state.topViewport.innerWidth || 0,
366
+ innerHeight: state.topViewport.innerHeight || 0,
367
+ outerWidth: state.topViewport.outerWidth || 0,
368
+ outerHeight: state.topViewport.outerHeight || 0,
369
+ visualWidth: state.topViewport.visualWidth || 0,
370
+ visualHeight: state.topViewport.visualHeight || 0,
371
+ screenAvailWidth: state.topViewport.screenAvailWidth || 0,
372
+ screenAvailHeight: state.topViewport.screenAvailHeight || 0,
373
+ devicePixelRatio: state.topViewport.devicePixelRatio || 0
374
+ }
375
+ : null,
376
+ viewportDiagnostics: state.viewportDiagnostics || null,
377
+ collapsed: Boolean(state.collapsed)
378
+ };
379
+ }
380
+
381
+ export function compactViewportHealthResult(result = null) {
382
+ if (!result) return null;
383
+ return {
384
+ ok: Boolean(result.ok),
385
+ collapsed: Boolean(result.collapsed),
386
+ recovered: Boolean(result.recovered),
387
+ reason: result.reason || null,
388
+ state: compactViewportState(result.state),
389
+ before: compactViewportState(result.before),
390
+ repair: result.repair
391
+ ? {
392
+ ok: Boolean(result.repair.ok),
393
+ applied: Boolean(result.repair.applied),
394
+ current_state: result.repair.current_state || null,
395
+ sequence: result.repair.sequence || [],
396
+ attempts: (result.repair.attempts || []).map((attempt) => ({
397
+ ok: Boolean(attempt.ok),
398
+ windowState: attempt.windowState,
399
+ error: attempt.error || null
400
+ }))
401
+ }
402
+ : null,
403
+ error: result.error || null
404
+ };
405
+ }
406
+
407
+ export async function ensureHealthyViewport(client, {
408
+ roots = {},
409
+ root = "frame",
410
+ frameOwnerRoot = "frameOwner",
411
+ reason = "viewport_recovery",
412
+ repair = true,
413
+ recoveryDelayMs = 900
414
+ } = {}) {
415
+ const before = await readViewportState(client, {
416
+ roots,
417
+ root,
418
+ frameOwnerRoot
419
+ });
420
+ if (!before.ok) {
421
+ return {
422
+ ok: false,
423
+ collapsed: false,
424
+ recovered: false,
425
+ reason,
426
+ state: before,
427
+ error: before.error || "viewport state could not be read"
428
+ };
429
+ }
430
+
431
+ if (!isListViewportCollapsed(before)) {
432
+ return {
433
+ ok: true,
434
+ collapsed: false,
435
+ recovered: false,
436
+ reason,
437
+ state: before
438
+ };
439
+ }
440
+
441
+ if (!repair) {
442
+ return {
443
+ ok: false,
444
+ collapsed: true,
445
+ recovered: false,
446
+ reason,
447
+ before,
448
+ state: before,
449
+ error: "viewport collapsed and repair disabled"
450
+ };
451
+ }
452
+
453
+ const repairResult = await toggleWindowStateForViewportRecovery(client, { reason });
454
+ if (recoveryDelayMs > 0) await sleep(recoveryDelayMs);
455
+ const after = await readViewportState(client, {
456
+ roots,
457
+ root,
458
+ frameOwnerRoot
459
+ });
460
+ const stillCollapsed = isListViewportCollapsed(after);
461
+ return {
462
+ ok: after.ok && !stillCollapsed,
463
+ collapsed: stillCollapsed,
464
+ recovered: after.ok && !stillCollapsed && repairResult.applied,
465
+ reason,
466
+ before,
467
+ state: after,
468
+ repair: repairResult,
469
+ error: after.ok && !stillCollapsed
470
+ ? null
471
+ : "viewport collapsed after recovery attempt"
472
+ };
473
+ }
474
+
475
+ export function createViewportRunGuard({
476
+ client,
477
+ domain = "boss",
478
+ root = "frame",
479
+ frameOwnerRoot = "frameOwner",
480
+ runControl = null,
481
+ getRoots = null,
482
+ rootNodesFromState = (rootState) => rootState?.rootNodes || rootState?.roots || rootState || {},
483
+ repair = true,
484
+ maxEvents = 10
485
+ } = {}) {
486
+ if (!client) throw new Error("createViewportRunGuard requires a guarded CDP client");
487
+ const events = [];
488
+ const stats = {
489
+ checks: 0,
490
+ recoveries: 0,
491
+ failures: 0
492
+ };
493
+
494
+ function recordEvent(phase, health) {
495
+ const compact = compactViewportHealthResult(health);
496
+ const shouldRecord = Boolean(health?.recovered || !health?.ok || health?.collapsed);
497
+ if (!shouldRecord) return compact;
498
+ const event = {
499
+ phase,
500
+ at: new Date().toISOString(),
501
+ ...compact
502
+ };
503
+ events.push(event);
504
+ if (events.length > maxEvents) events.shift();
505
+ if (runControl) {
506
+ runControl.checkpoint({
507
+ viewport_health: event,
508
+ viewport_health_events: events.slice(),
509
+ viewport_health_stats: { ...stats }
510
+ });
511
+ }
512
+ return compact;
513
+ }
514
+
515
+ async function ensure(rootState, {
516
+ phase = "run",
517
+ reason = `${domain}:${phase}`
518
+ } = {}) {
519
+ let currentRootState = rootState;
520
+ if (!currentRootState && typeof getRoots === "function") {
521
+ currentRootState = await getRoots(client);
522
+ }
523
+ const roots = rootNodesFromState(currentRootState);
524
+ stats.checks += 1;
525
+ const health = await ensureHealthyViewport(client, {
526
+ roots,
527
+ root,
528
+ frameOwnerRoot,
529
+ reason,
530
+ repair
531
+ });
532
+ if (health.recovered) stats.recoveries += 1;
533
+ if (!health.ok) stats.failures += 1;
534
+ const compact = recordEvent(phase, health);
535
+ if (!health.ok) {
536
+ const error = new Error(`${String(domain).toUpperCase()}_LIST_VIEWPORT_COLLAPSED`);
537
+ error.code = "LIST_VIEWPORT_COLLAPSED";
538
+ error.domain = domain;
539
+ error.phase = phase;
540
+ error.viewport_health = compact;
541
+ throw error;
542
+ }
543
+ if (health.recovered && typeof getRoots === "function") {
544
+ currentRootState = await getRoots(client);
545
+ }
546
+ return {
547
+ rootState: currentRootState,
548
+ health,
549
+ compact,
550
+ stats: { ...stats },
551
+ events: events.slice()
552
+ };
553
+ }
554
+
555
+ return {
556
+ ensure,
557
+ getStats() {
558
+ return { ...stats };
559
+ },
560
+ getEvents() {
561
+ return events.slice();
562
+ }
563
+ };
564
+ }