@web-auto/camo 0.1.24 → 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.
@@ -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
- if (shouldNavigateToSeed(newest.url, seedUrl)) {
183
- await callApiWithTimeout('goto', { profileId, url: seedUrl }, navigationTimeoutMs);
184
- if (openDelayMs > 0) await sleep(Math.min(openDelayMs, 1200));
185
- }
186
- const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
187
- if (!syncResult?.ok) {
188
- throw new Error(syncResult?.message || 'sync_window_viewport failed');
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 tryOpenTabWithShortcut(profileId, timeoutMs) {
193
- const candidates = process.platform === 'darwin'
194
- ? ['Meta+t', 'Control+t']
195
- : ['Control+t', 'Meta+t'];
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
- for (const key of candidates) {
246
+
247
+ while (Date.now() - startedAt <= waitMs) {
198
248
  try {
199
- await callApiWithTimeout('keyboard:press', { profileId, key }, timeoutMs);
200
- return { ok: true, key };
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
- return { ok: false, error: lastError };
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 hasNewTab = async () => {
219
- const listed = await callApiWithTimeout('page:list', { profileId }, apiTimeoutMs);
220
- const { pages } = extractPageList(listed);
221
- return pages.length > beforeCount;
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 shortcutResult = await tryOpenTabWithShortcut(profileId, shortcutTimeoutMs);
231
- if (shortcutResult.ok) {
232
- await settle();
233
- if (await hasNewTab()) {
234
- await seedNewestTabIfNeeded({
235
- profileId,
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, apiTimeoutMs);
361
+ await callApiWithTimeout('newPage', payload, openCommandTimeoutMs);
253
362
  await settle();
254
- if (await hasNewTab()) {
255
- await seedNewestTabIfNeeded({
256
- profileId,
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 openDelayMs = Math.max(0, Number(params.openDelayMs ?? 350) || 350);
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
- return asErrorPayload('OPERATION_FAILED', 'new_tab_failed', {
343
- tabCount,
344
- beforeCount,
345
- afterCount: pages.length,
346
- seedUrl: fallbackSeedUrl || null,
347
- mode: openResult.mode || null,
348
- reason: openResult.error?.message || 'cannot open new tab',
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
- await callApiWithTimeout('page:switch', { profileId, index: pageIndex }, apiTimeoutMs);
368
- if (shouldNavigateToSeed(page.url, fallbackSeedUrl)) {
369
- await callApiWithTimeout('goto', { profileId, url: fallbackSeedUrl }, navigationTimeoutMs);
370
- if (openDelayMs > 0) await sleep(Math.min(openDelayMs, 1200));
371
- }
372
- const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
373
- if (!syncResult?.ok) {
374
- return asErrorPayload('OPERATION_FAILED', 'tab viewport sync failed', {
375
- pageIndex,
376
- syncResult,
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
- await callApiWithTimeout('page:switch', {
395
- profileId,
396
- index: Number(slots[0].tabRealIndex),
397
- }, apiTimeoutMs);
398
- const syncResult = await syncTabViewportIfNeeded({ profileId, syncConfig });
399
- if (!syncResult?.ok) {
400
- return asErrorPayload('OPERATION_FAILED', 'tab viewport sync failed', { slotIndex: 1, syncResult });
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,