@web-auto/camo 0.1.23 → 0.1.25
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/cli.mjs +9 -0
- package/src/commands/browser.mjs +9 -7
- package/src/container/change-notifier.mjs +90 -39
- package/src/container/runtime-core/operations/index.mjs +108 -48
- package/src/container/runtime-core/operations/tab-pool.mjs +301 -99
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +762 -0
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +762 -0
- package/src/container/runtime-core/operations/viewport.mjs +46 -0
- package/src/container/runtime-core/subscription.mjs +72 -7
- package/src/container/runtime-core/validation.mjs +61 -4
- package/src/container/subscription-registry.mjs +1 -1
- package/src/core/utils.mjs +4 -0
- package/src/services/browser-service/index.js +27 -10
- package/src/services/browser-service/index.js.bak +671 -0
- package/src/services/browser-service/internal/BrowserSession.input.test.js +33 -0
- package/src/services/browser-service/internal/BrowserSession.js +34 -2
- package/src/services/browser-service/internal/browser-session/input-ops.js +27 -1
- package/src/services/browser-service/internal/browser-session/page-management.js +152 -36
- package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -0
- package/src/services/controller/controller.js +1 -1
- package/src/services/controller/transport.js +8 -1
- package/src/utils/args.mjs +1 -0
- package/src/utils/browser-service.mjs +13 -1
- package/src/utils/command-log.mjs +64 -0
- package/src/utils/config.mjs +1 -1
- package/src/utils/help.mjs +3 -3
|
@@ -9,6 +9,7 @@ const DEFAULT_VIEWPORT_API_TIMEOUT_MS = 8000;
|
|
|
9
9
|
const DEFAULT_VIEWPORT_SETTLE_MS = 120;
|
|
10
10
|
const DEFAULT_VIEWPORT_ATTEMPTS = 1;
|
|
11
11
|
const DEFAULT_VIEWPORT_TOLERANCE_PX = 3;
|
|
12
|
+
const RUNTIME_LAYER_TIMEOUT_MS = 10000;
|
|
12
13
|
|
|
13
14
|
function sleep(ms) {
|
|
14
15
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -57,6 +58,43 @@ function resolveViewportSyncConfig({ params = {}, inherited = null }) {
|
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 双重超时保护:在 runtime 层强制超时,即使 fetch 超时失效
|
|
65
|
+
*/
|
|
66
|
+
async function withRuntimeLayerTimeout(promise, timeoutMs, timeoutMessage) {
|
|
67
|
+
let runtimeTimer = null;
|
|
68
|
+
|
|
69
|
+
return Promise.race([
|
|
70
|
+
promise,
|
|
71
|
+
new Promise((_, reject) => {
|
|
72
|
+
runtimeTimer = setTimeout(() => {
|
|
73
|
+
console.error(`[RuntimeLayerTimeout] ${timeoutMessage}`);
|
|
74
|
+
reject(new Error(`Runtime layer timeout after ${timeoutMs}ms: ${timeoutMessage}`));
|
|
75
|
+
}, timeoutMs);
|
|
76
|
+
}),
|
|
77
|
+
]).finally(() => {
|
|
78
|
+
if (runtimeTimer) clearTimeout(runtimeTimer);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 带强制 runtime 层超时的 API 调用
|
|
84
|
+
*/
|
|
85
|
+
async function callApiWithRuntimeTimeout(action, payload, timeoutMs, runtimeTimeoutMs) {
|
|
86
|
+
const effectiveTimeoutMs = resolveTimeoutMs(timeoutMs, DEFAULT_API_TIMEOUT_MS);
|
|
87
|
+
const effectiveRuntimeTimeoutMs = Math.min(effectiveTimeoutMs + 5000, RUNTIME_LAYER_TIMEOUT_MS);
|
|
88
|
+
|
|
89
|
+
console.log(`[callApiWithRuntimeTimeout] ${action} timeout: ${effectiveTimeoutMs}ms, runtime: ${effectiveRuntimeTimeoutMs}ms`);
|
|
90
|
+
|
|
91
|
+
return withRuntimeLayerTimeout(
|
|
92
|
+
callAPI(action, payload),
|
|
93
|
+
effectiveRuntimeTimeoutMs,
|
|
94
|
+
`${action} runtime timeout after ${effectiveRuntimeTimeoutMs}ms`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
async function callApiWithTimeout(action, payload, timeoutMs) {
|
|
61
99
|
const effectiveTimeoutMs = resolveTimeoutMs(timeoutMs, DEFAULT_API_TIMEOUT_MS);
|
|
62
100
|
return withTimeout(
|
|
@@ -179,47 +217,131 @@ async function seedNewestTabIfNeeded({
|
|
|
179
217
|
if (Number(activeIndex) !== targetIndex) {
|
|
180
218
|
await callApiWithTimeout('page:switch', { profileId, index: targetIndex }, apiTimeoutMs);
|
|
181
219
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
220
|
+
try {
|
|
221
|
+
if (shouldNavigateToSeed(newest.url, seedUrl)) {
|
|
222
|
+
await callApiWithTimeout('goto', { profileId, url: seedUrl }, navigationTimeoutMs);
|
|
223
|
+
if (openDelayMs > 0) await sleep(Math.min(openDelayMs, 1200));
|
|
224
|
+
}
|
|
225
|
+
const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
|
|
226
|
+
if (!syncResult?.ok) {
|
|
227
|
+
throw new Error(syncResult?.message || 'sync_window_viewport failed');
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// Best-effort seeding; failing to navigate/sync shouldn't block tab pool init.
|
|
189
231
|
}
|
|
190
232
|
}
|
|
191
233
|
|
|
192
|
-
async function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
async function waitForTabCountIncrease({
|
|
235
|
+
profileId,
|
|
236
|
+
beforeCount,
|
|
237
|
+
apiTimeoutMs,
|
|
238
|
+
maxWaitMs,
|
|
239
|
+
pollMs = 250,
|
|
240
|
+
}) {
|
|
241
|
+
const startedAt = Date.now();
|
|
242
|
+
const effectivePollMs = Math.max(80, Number(pollMs) || 250);
|
|
243
|
+
const waitMs = Math.max(400, Number(maxWaitMs) || 4000);
|
|
244
|
+
const listTimeoutMs = Math.max(1000, Math.min(resolveTimeoutMs(apiTimeoutMs, DEFAULT_API_TIMEOUT_MS), 8000));
|
|
196
245
|
let lastError = null;
|
|
197
|
-
|
|
246
|
+
|
|
247
|
+
while (Date.now() - startedAt <= waitMs) {
|
|
198
248
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
249
|
+
const pollStartTime = Date.now();
|
|
250
|
+
const listed = await callApiWithRuntimeTimeout('page:list', { profileId }, listTimeoutMs, RUNTIME_LAYER_TIMEOUT_MS);
|
|
251
|
+
console.log(`[waitForTabCountIncrease] page:list took ${Date.now() - pollStartTime}ms`);
|
|
252
|
+
const { pages } = extractPageList(listed);
|
|
253
|
+
if (pages.length > beforeCount) {
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
elapsedMs: Date.now() - startedAt,
|
|
257
|
+
afterCount: pages.length,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
201
260
|
} catch (err) {
|
|
202
261
|
lastError = err;
|
|
203
262
|
}
|
|
263
|
+
await sleep(effectivePollMs);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
elapsedMs: Date.now() - startedAt,
|
|
269
|
+
error: lastError,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function hydrateBlankNewestTab({
|
|
274
|
+
profileId,
|
|
275
|
+
beforeCount,
|
|
276
|
+
seedUrl,
|
|
277
|
+
openDelayMs,
|
|
278
|
+
apiTimeoutMs,
|
|
279
|
+
navigationTimeoutMs,
|
|
280
|
+
tabAppearTimeoutMs,
|
|
281
|
+
syncConfig,
|
|
282
|
+
}) {
|
|
283
|
+
if (!seedUrl) {
|
|
284
|
+
return { ok: false, reason: 'missing_seed_url' };
|
|
285
|
+
}
|
|
286
|
+
const listed = await callApiWithTimeout('page:list', { profileId }, apiTimeoutMs);
|
|
287
|
+
const { pages } = extractPageList(listed);
|
|
288
|
+
const candidates = [...pages]
|
|
289
|
+
.filter((page) => Number.isFinite(Number(page?.index)))
|
|
290
|
+
.sort((a, b) => Number(b.index) - Number(a.index));
|
|
291
|
+
const newest = candidates[0] || null;
|
|
292
|
+
if (!newest) {
|
|
293
|
+
return { ok: false, reason: 'no_pages' };
|
|
204
294
|
}
|
|
205
|
-
|
|
295
|
+
const currentCount = pages.length;
|
|
296
|
+
if (currentCount <= beforeCount) {
|
|
297
|
+
return { ok: false, reason: 'count_not_increased' };
|
|
298
|
+
}
|
|
299
|
+
const currentUrl = String(newest.url || '').trim().toLowerCase();
|
|
300
|
+
const isBlank = !currentUrl || currentUrl === 'about:blank';
|
|
301
|
+
if (!isBlank) {
|
|
302
|
+
return { ok: true, reason: 'newest_already_navigated', afterCount: currentCount };
|
|
303
|
+
}
|
|
304
|
+
await callApiWithTimeout('page:switch', { profileId, index: Number(newest.index) }, apiTimeoutMs);
|
|
305
|
+
await callApiWithTimeout('goto', { profileId, url: seedUrl }, navigationTimeoutMs);
|
|
306
|
+
if (openDelayMs > 0) {
|
|
307
|
+
await sleep(Math.min(openDelayMs, 1200));
|
|
308
|
+
}
|
|
309
|
+
const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
|
|
310
|
+
if (!syncResult?.ok) {
|
|
311
|
+
throw new Error(syncResult?.message || 'sync_window_viewport failed');
|
|
312
|
+
}
|
|
313
|
+
const after = await waitForTabCountIncrease({
|
|
314
|
+
profileId,
|
|
315
|
+
beforeCount,
|
|
316
|
+
apiTimeoutMs,
|
|
317
|
+
maxWaitMs: Math.max(1500, Math.min(tabAppearTimeoutMs, 6000)),
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
reason: 'blank_tab_hydrated',
|
|
322
|
+
afterCount: after?.afterCount || currentCount,
|
|
323
|
+
};
|
|
206
324
|
}
|
|
207
325
|
|
|
208
326
|
async function openTabBestEffort({
|
|
209
327
|
profileId,
|
|
210
328
|
seedUrl,
|
|
329
|
+
seedOnOpen = true,
|
|
330
|
+
shortcutOnly = false,
|
|
211
331
|
openDelayMs,
|
|
212
332
|
beforeCount,
|
|
213
333
|
apiTimeoutMs,
|
|
214
334
|
navigationTimeoutMs,
|
|
215
335
|
shortcutTimeoutMs,
|
|
336
|
+
tabAppearTimeoutMs,
|
|
216
337
|
syncConfig,
|
|
217
338
|
}) {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
339
|
+
const waitForTab = async () => waitForTabCountIncrease({
|
|
340
|
+
profileId,
|
|
341
|
+
beforeCount,
|
|
342
|
+
apiTimeoutMs,
|
|
343
|
+
maxWaitMs: tabAppearTimeoutMs,
|
|
344
|
+
});
|
|
223
345
|
const settle = async () => {
|
|
224
346
|
if (openDelayMs > 0) {
|
|
225
347
|
await new Promise((resolve) => setTimeout(resolve, openDelayMs));
|
|
@@ -227,57 +349,20 @@ async function openTabBestEffort({
|
|
|
227
349
|
};
|
|
228
350
|
|
|
229
351
|
let openError = null;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
seedUrl,
|
|
237
|
-
openDelayMs,
|
|
238
|
-
apiTimeoutMs,
|
|
239
|
-
navigationTimeoutMs,
|
|
240
|
-
syncConfig,
|
|
241
|
-
});
|
|
242
|
-
return { ok: true, mode: `shortcut:${shortcutResult.key}`, error: null };
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
openError = shortcutResult.error;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const payload = seedUrl
|
|
352
|
+
const openCommandTimeoutMs = Math.max(
|
|
353
|
+
60000,
|
|
354
|
+
resolveTimeoutMs(apiTimeoutMs, DEFAULT_API_TIMEOUT_MS),
|
|
355
|
+
resolveTimeoutMs(tabAppearTimeoutMs, 0) + Math.max(30000, Math.min(openDelayMs || 0, 20000)),
|
|
356
|
+
);
|
|
357
|
+
const payload = seedOnOpen && seedUrl
|
|
249
358
|
? { profileId, url: seedUrl }
|
|
250
359
|
: { profileId };
|
|
251
360
|
try {
|
|
252
|
-
await callApiWithTimeout('newPage', payload,
|
|
361
|
+
await callApiWithTimeout('newPage', payload, openCommandTimeoutMs);
|
|
253
362
|
await settle();
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
seedUrl,
|
|
258
|
-
openDelayMs,
|
|
259
|
-
apiTimeoutMs,
|
|
260
|
-
navigationTimeoutMs,
|
|
261
|
-
syncConfig,
|
|
262
|
-
});
|
|
263
|
-
return { ok: true, mode: 'newPage', error: null };
|
|
264
|
-
}
|
|
265
|
-
} catch (err) {
|
|
266
|
-
openError = err;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const popupResult = await callApiWithTimeout('evaluate', {
|
|
271
|
-
profileId,
|
|
272
|
-
script: `(() => {
|
|
273
|
-
const popup = window.open(${JSON.stringify(seedUrl || 'about:blank')}, '_blank');
|
|
274
|
-
return { opened: !!popup };
|
|
275
|
-
})()`,
|
|
276
|
-
}, apiTimeoutMs);
|
|
277
|
-
const popupData = popupResult?.result || popupResult || {};
|
|
278
|
-
if (Boolean(popupData?.opened || popupData?.ok)) {
|
|
279
|
-
await settle();
|
|
280
|
-
if (await hasNewTab()) {
|
|
363
|
+
const newPageOpened = await waitForTab();
|
|
364
|
+
if (newPageOpened.ok) {
|
|
365
|
+
if (seedOnOpen && seedUrl) {
|
|
281
366
|
await seedNewestTabIfNeeded({
|
|
282
367
|
profileId,
|
|
283
368
|
seedUrl,
|
|
@@ -286,8 +371,24 @@ async function openTabBestEffort({
|
|
|
286
371
|
navigationTimeoutMs,
|
|
287
372
|
syncConfig,
|
|
288
373
|
});
|
|
289
|
-
return { ok: true, mode: 'window.open', error: null };
|
|
290
374
|
}
|
|
375
|
+
return { ok: true, mode: 'newPage', error: null };
|
|
376
|
+
}
|
|
377
|
+
const hydrated = await hydrateBlankNewestTab({
|
|
378
|
+
profileId,
|
|
379
|
+
beforeCount,
|
|
380
|
+
seedUrl,
|
|
381
|
+
openDelayMs,
|
|
382
|
+
apiTimeoutMs,
|
|
383
|
+
navigationTimeoutMs,
|
|
384
|
+
tabAppearTimeoutMs,
|
|
385
|
+
syncConfig,
|
|
386
|
+
}).catch((error) => ({ ok: false, error }));
|
|
387
|
+
if (hydrated?.ok) {
|
|
388
|
+
return { ok: true, mode: 'newPage_hydrated_blank', error: null };
|
|
389
|
+
}
|
|
390
|
+
if (hydrated?.error) {
|
|
391
|
+
openError = hydrated.error;
|
|
291
392
|
}
|
|
292
393
|
} catch (err) {
|
|
293
394
|
openError = err;
|
|
@@ -301,11 +402,22 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
|
|
|
301
402
|
|
|
302
403
|
if (action === 'ensure_tab_pool') {
|
|
303
404
|
const tabCount = Math.max(1, Number(params.tabCount ?? params.count ?? 1) || 1);
|
|
304
|
-
const
|
|
405
|
+
const minOpenDelayMs = Math.max(0, Number(params.minDelayMs ?? params.minOpenDelayMs ?? 0) || 0);
|
|
406
|
+
const openDelayMs = Math.max(
|
|
407
|
+
minOpenDelayMs,
|
|
408
|
+
Math.max(0, Number(params.openDelayMs ?? 350) || 350),
|
|
409
|
+
);
|
|
305
410
|
const normalizeTabs = params.normalizeTabs === true;
|
|
411
|
+
const seedOnOpen = params.seedOnOpen !== false;
|
|
412
|
+
const shortcutOnly = params.shortcutOnly === true;
|
|
413
|
+
const reuseOnly = params.reuseOnly === true;
|
|
306
414
|
const apiTimeoutMs = resolveTimeoutMs(params.apiTimeoutMs, DEFAULT_API_TIMEOUT_MS);
|
|
307
415
|
const navigationTimeoutMs = resolveTimeoutMs(params.navigationTimeoutMs ?? params.gotoTimeoutMs, DEFAULT_NAV_TIMEOUT_MS);
|
|
308
416
|
const shortcutTimeoutMs = resolveTimeoutMs(params.shortcutTimeoutMs, SHORTCUT_OPEN_TIMEOUT_MS);
|
|
417
|
+
const tabAppearTimeoutMs = resolveTimeoutMs(
|
|
418
|
+
params.tabAppearTimeoutMs,
|
|
419
|
+
Math.max(20000, openDelayMs + 15000),
|
|
420
|
+
);
|
|
309
421
|
const syncConfig = resolveViewportSyncConfig({ params });
|
|
310
422
|
const configuredSeedUrl = normalizeSeedUrl(String(params.url || '').trim());
|
|
311
423
|
|
|
@@ -323,33 +435,57 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
|
|
|
323
435
|
if (recoveredListUrl) fallbackSeedUrl = recoveredListUrl;
|
|
324
436
|
}
|
|
325
437
|
fallbackSeedUrl = normalizeSeedUrl(fallbackSeedUrl);
|
|
438
|
+
let openFailures = 0;
|
|
439
|
+
const maxOpenFailures = Math.max(3, tabCount);
|
|
326
440
|
|
|
327
441
|
while (pages.length < tabCount) {
|
|
442
|
+
if (reuseOnly) {
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
328
445
|
const beforeCount = pages.length;
|
|
329
446
|
const openResult = await openTabBestEffort({
|
|
330
447
|
profileId,
|
|
331
448
|
seedUrl: fallbackSeedUrl,
|
|
449
|
+
seedOnOpen,
|
|
450
|
+
shortcutOnly,
|
|
332
451
|
openDelayMs,
|
|
333
452
|
beforeCount,
|
|
334
453
|
apiTimeoutMs,
|
|
335
454
|
navigationTimeoutMs,
|
|
336
455
|
shortcutTimeoutMs,
|
|
456
|
+
tabAppearTimeoutMs,
|
|
337
457
|
syncConfig,
|
|
338
458
|
});
|
|
339
459
|
listed = await callApiWithTimeout('page:list', { profileId }, apiTimeoutMs);
|
|
340
460
|
({ pages, activeIndex } = extractPageList(listed));
|
|
341
461
|
if (!openResult.ok || pages.length <= beforeCount) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
462
|
+
openFailures += 1;
|
|
463
|
+
if (openFailures >= maxOpenFailures) {
|
|
464
|
+
return asErrorPayload('OPERATION_FAILED', 'new_tab_failed', {
|
|
465
|
+
tabCount,
|
|
466
|
+
beforeCount,
|
|
467
|
+
afterCount: pages.length,
|
|
468
|
+
seedUrl: fallbackSeedUrl || null,
|
|
469
|
+
mode: openResult.mode || null,
|
|
470
|
+
reason: openResult.error?.message || 'cannot open new tab',
|
|
471
|
+
attempts: openFailures,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (openDelayMs > 0) {
|
|
475
|
+
await sleep(Math.min(openDelayMs, 1200));
|
|
476
|
+
} else {
|
|
477
|
+
await sleep(300);
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
350
480
|
}
|
|
351
481
|
}
|
|
352
482
|
|
|
483
|
+
if (reuseOnly && pages.length === 0) {
|
|
484
|
+
return asErrorPayload('TAB_POOL_EMPTY', 'reuse_only_tab_pool_requires_existing_tabs', {
|
|
485
|
+
tabCount,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
353
489
|
const sortedPages = [...pages].sort((a, b) => Number(a.index) - Number(b.index));
|
|
354
490
|
const activePage = sortedPages.find((item) => Number(item.index) === Number(activeIndex)) || null;
|
|
355
491
|
const selected = [
|
|
@@ -357,24 +493,55 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
|
|
|
357
493
|
...sortedPages.filter((item) => Number(item.index) !== Number(activeIndex)),
|
|
358
494
|
].slice(0, tabCount);
|
|
359
495
|
|
|
360
|
-
const forceNormalizeFromDetail = Boolean(!configuredSeedUrl && isXhsDetailUrl(defaultSeedUrl));
|
|
361
|
-
const shouldNormalizeSlots = Boolean(fallbackSeedUrl) && (normalizeTabs || forceNormalizeFromDetail);
|
|
496
|
+
const forceNormalizeFromDetail = Boolean(seedOnOpen && !configuredSeedUrl && isXhsDetailUrl(defaultSeedUrl));
|
|
497
|
+
const shouldNormalizeSlots = seedOnOpen && Boolean(fallbackSeedUrl) && (normalizeTabs || forceNormalizeFromDetail);
|
|
362
498
|
|
|
363
499
|
if (shouldNormalizeSlots) {
|
|
364
500
|
for (const page of selected) {
|
|
365
501
|
const pageIndex = Number(page.index);
|
|
366
502
|
if (!Number.isFinite(pageIndex)) continue;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
503
|
+
try {
|
|
504
|
+
await callApiWithTimeout('page:switch', { profileId, index: pageIndex }, apiTimeoutMs);
|
|
505
|
+
if (shouldNavigateToSeed(page.url, fallbackSeedUrl)) {
|
|
506
|
+
await callApiWithTimeout('goto', { profileId, url: fallbackSeedUrl }, navigationTimeoutMs);
|
|
507
|
+
if (openDelayMs > 0) await sleep(Math.min(openDelayMs, 1200));
|
|
508
|
+
}
|
|
509
|
+
const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
|
|
510
|
+
if (!syncResult?.ok) {
|
|
511
|
+
throw new Error(syncResult?.message || 'tab viewport sync failed');
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
const activePage = selected.find((item) => Number(item.index) === Number(activeIndex)) || selected[0] || null;
|
|
515
|
+
const slots = activePage
|
|
516
|
+
? [{
|
|
517
|
+
slotIndex: 1,
|
|
518
|
+
tabRealIndex: Number(activePage.index),
|
|
519
|
+
url: String(activePage.url || ''),
|
|
520
|
+
}]
|
|
521
|
+
: [];
|
|
522
|
+
if (runtimeState) {
|
|
523
|
+
runtimeState.tabPool = {
|
|
524
|
+
slots,
|
|
525
|
+
cursor: 0,
|
|
526
|
+
count: slots.length,
|
|
527
|
+
syncConfig,
|
|
528
|
+
apiTimeoutMs,
|
|
529
|
+
initializedAt: new Date().toISOString(),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
ok: true,
|
|
534
|
+
code: 'OPERATION_DEGRADED',
|
|
535
|
+
message: 'ensure_tab_pool degraded to active tab',
|
|
536
|
+
data: {
|
|
537
|
+
tabCount: slots.length,
|
|
538
|
+
normalized: false,
|
|
539
|
+
degraded: true,
|
|
540
|
+
reason: err?.message || 'page switch failed',
|
|
541
|
+
slots,
|
|
542
|
+
pages: activePage ? [activePage] : [],
|
|
543
|
+
},
|
|
544
|
+
};
|
|
378
545
|
}
|
|
379
546
|
}
|
|
380
547
|
listed = await callApiWithTimeout('page:list', { profileId }, apiTimeoutMs);
|
|
@@ -391,13 +558,39 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
|
|
|
391
558
|
}));
|
|
392
559
|
|
|
393
560
|
if (slots.length > 0) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
561
|
+
try {
|
|
562
|
+
await callApiWithTimeout('page:switch', {
|
|
563
|
+
profileId,
|
|
564
|
+
index: Number(slots[0].tabRealIndex),
|
|
565
|
+
}, apiTimeoutMs);
|
|
566
|
+
const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
|
|
567
|
+
if (!syncResult?.ok) {
|
|
568
|
+
throw new Error(syncResult?.message || 'tab viewport sync failed');
|
|
569
|
+
}
|
|
570
|
+
} catch (err) {
|
|
571
|
+
if (runtimeState) {
|
|
572
|
+
runtimeState.tabPool = {
|
|
573
|
+
slots,
|
|
574
|
+
cursor: 0,
|
|
575
|
+
count: slots.length,
|
|
576
|
+
syncConfig,
|
|
577
|
+
apiTimeoutMs,
|
|
578
|
+
initializedAt: new Date().toISOString(),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
code: 'OPERATION_DEGRADED',
|
|
584
|
+
message: 'ensure_tab_pool degraded on final switch',
|
|
585
|
+
data: {
|
|
586
|
+
tabCount: slots.length,
|
|
587
|
+
normalized: false,
|
|
588
|
+
degraded: true,
|
|
589
|
+
reason: err?.message || 'page switch failed',
|
|
590
|
+
slots,
|
|
591
|
+
pages: slots,
|
|
592
|
+
},
|
|
593
|
+
};
|
|
401
594
|
}
|
|
402
595
|
}
|
|
403
596
|
|
|
@@ -410,6 +603,13 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
|
|
|
410
603
|
apiTimeoutMs,
|
|
411
604
|
initializedAt: new Date().toISOString(),
|
|
412
605
|
};
|
|
606
|
+
runtimeState.currentTab = slots[0]
|
|
607
|
+
? {
|
|
608
|
+
slotIndex: Number(slots[0].slotIndex),
|
|
609
|
+
tabRealIndex: Number(slots[0].tabRealIndex),
|
|
610
|
+
url: String(slots[0].url || ''),
|
|
611
|
+
}
|
|
612
|
+
: null;
|
|
413
613
|
}
|
|
414
614
|
|
|
415
615
|
return {
|
|
@@ -470,9 +670,10 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
|
|
|
470
670
|
});
|
|
471
671
|
}
|
|
472
672
|
runtimeState.currentTab = {
|
|
473
|
-
slotIndex: selected.slotIndex,
|
|
673
|
+
slotIndex: Number(selected.slotIndex),
|
|
474
674
|
tabRealIndex: targetIndex,
|
|
475
675
|
activeIndex,
|
|
676
|
+
url: String(selected.url || ''),
|
|
476
677
|
};
|
|
477
678
|
|
|
478
679
|
return {
|
|
@@ -528,9 +729,10 @@ export async function executeTabPoolOperation({ profileId, action, params = {},
|
|
|
528
729
|
});
|
|
529
730
|
}
|
|
530
731
|
runtimeState.currentTab = {
|
|
531
|
-
slotIndex: slot.slotIndex,
|
|
732
|
+
slotIndex: Number(slot.slotIndex),
|
|
532
733
|
tabRealIndex: targetIndex,
|
|
533
734
|
activeIndex,
|
|
735
|
+
url: String(slot.url || ''),
|
|
534
736
|
};
|
|
535
737
|
return {
|
|
536
738
|
ok: true,
|