@reconcrap/boss-recommend-mcp 2.0.4 → 2.0.6
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/chat-mcp.js +2 -1
- package/src/core/browser/index.js +1 -0
- package/src/core/self-heal/index.js +128 -3
- package/src/core/self-heal/viewport.js +564 -0
- package/src/domains/chat/run-service.js +52 -8
- package/src/domains/recommend/detail.js +189 -6
- package/src/domains/recommend/roots.js +4 -2
- package/src/domains/recommend/run-service.js +51 -7
- package/src/domains/recruit/roots.js +2 -1
- package/src/domains/recruit/run-service.js +34 -3
- package/src/index.js +2 -1
- package/src/recommend-mcp.js +2 -1
|
@@ -0,0 +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", "normal"]
|
|
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
|
+
}
|