@sovovs/bycli 2.1.39 → 2.1.41

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 (47) hide show
  1. package/cli-manifest.json +56 -3
  2. package/clis/ima/knowledge-list.js +48 -0
  3. package/clis/ima/knowledge.js +23 -0
  4. package/clis/ima/native-api.js +67 -10
  5. package/clis/ima/native-client.js +23 -7
  6. package/clis/ima/utils.js +1 -0
  7. package/clis/weixin/_wechat/article-artifact.js +55 -0
  8. package/clis/weixin/_wechat/article-identity.js +27 -0
  9. package/clis/weixin/_wechat/publish-analysis.js +8 -4
  10. package/clis/weixin/_wechat/publish-download.js +3 -1
  11. package/clis/weixin/download-publish-data.js +20 -0
  12. package/clis/weixin/download.js +15 -2
  13. package/dist/src/adapter-coordination.d.ts +26 -0
  14. package/dist/src/adapter-coordination.js +183 -0
  15. package/dist/src/adapter-coordination.test.d.ts +1 -0
  16. package/dist/src/adapter-execution-context.d.ts +6 -0
  17. package/dist/src/adapter-execution-context.js +8 -0
  18. package/dist/src/adapter-scheduler.d.ts +86 -0
  19. package/dist/src/adapter-scheduler.js +349 -0
  20. package/dist/src/adapter-scheduler.test.d.ts +1 -0
  21. package/dist/src/browser/daemon-client.d.ts +11 -0
  22. package/dist/src/browser/daemon-client.js +53 -1
  23. package/dist/src/browser/extension-capabilities.d.ts +1 -0
  24. package/dist/src/browser/extension-capabilities.js +18 -5
  25. package/dist/src/browser/page.d.ts +2 -1
  26. package/dist/src/browser/page.js +3 -0
  27. package/dist/src/build-manifest.js +1 -0
  28. package/dist/src/cli-argv-preprocess.d.ts +3 -0
  29. package/dist/src/cli-argv-preprocess.js +4 -0
  30. package/dist/src/commanderAdapter.js +11 -0
  31. package/dist/src/daemon.js +118 -0
  32. package/dist/src/discovery.js +1 -0
  33. package/dist/src/download/article-download.d.ts +2 -0
  34. package/dist/src/download/article-download.js +30 -4
  35. package/dist/src/errors.d.ts +3 -0
  36. package/dist/src/errors.js +5 -0
  37. package/dist/src/execution.d.ts +2 -0
  38. package/dist/src/execution.js +168 -105
  39. package/dist/src/help.d.ts +1 -0
  40. package/dist/src/help.js +40 -0
  41. package/dist/src/manifest-types.d.ts +4 -0
  42. package/dist/src/registry.d.ts +6 -0
  43. package/dist/src/registry.js +23 -0
  44. package/dist/src/serialization.d.ts +1 -0
  45. package/dist/src/serialization.js +1 -0
  46. package/dist/src/types.d.ts +2 -0
  47. package/package.json +3 -2
@@ -16,10 +16,11 @@ import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import { getUserClisDir } from './config-paths.js';
18
18
  import { executePipeline } from './pipeline/index.js';
19
- import { adapterLoadError, ArgumentError, CliError, CommandExecutionError, attachTraceReceipt, getErrorMessage } from './errors.js';
19
+ import { AdapterCoordinationError, adapterLoadError, ArgumentError, CliError, CommandExecutionError, TimeoutError, attachTraceReceipt, getErrorMessage } from './errors.js';
20
20
  import { shouldUseBrowserSession } from './capabilityRouting.js';
21
21
  import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
22
22
  import { resolveProfileContextId } from './browser/profile.js';
23
+ import { resolveAdapterLeaseContextId } from './browser/daemon-client.js';
23
24
  import { emitHook } from './hooks.js';
24
25
  import { log } from './logger.js';
25
26
  import { isElectronApp } from './electron-apps.js';
@@ -27,6 +28,7 @@ import { probeCDP, resolveElectronEndpoint } from './launcher.js';
27
28
  import { ObservationSession, exportObservationSession } from './observation/index.js';
28
29
  import { resolveAdapterSourcePath } from './adapter-source.js';
29
30
  import { canonicalizeManifestArgSchema, ManifestSchemaError } from './manifest-schema.js';
31
+ import { settleAdapterOperationAfterTimeout, withAdapterCommandLease } from './adapter-coordination.js';
30
32
  import { capturedRegistryValues, closeRegistryTransaction, createRegistryTransaction, finalizeRegistryTransaction, resetRegistryTransactionStateForTests, rollbackRegistryTransaction, runRegistryTransaction, transactionGroupsForKey, } from './registry-transaction.js';
31
33
  const _loadedModules = new Map();
32
34
  /** Independent cache-busting generation; retained when an import promise is discarded. */
@@ -402,6 +404,8 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
402
404
  let result;
403
405
  try {
404
406
  const resolvedBrowser = resolveBrowserRequirement(cmd, kwargs);
407
+ const adapterSession = normalizeAdapterSession(cmd, resolvedBrowser, opts.adapterSession);
408
+ const adapterQueueTimeoutSeconds = normalizeAdapterQueueTimeout(opts.adapterQueueTimeout, adapterSession);
405
409
  const userTimeoutSec = readUserTimeoutSeconds(cmd, kwargs);
406
410
  if (shouldUseBrowserSession(cmd, resolvedBrowser)) {
407
411
  const electron = isElectronApp(cmd.site);
@@ -424,126 +428,154 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
424
428
  const contextId = resolveProfileContextId(opts.profile);
425
429
  const internal = cmd;
426
430
  const siteSession = resolveSiteSession(cmd, opts.siteSession);
427
- const session = resolveAdapterBrowserSession(cmd, siteSession);
431
+ assertAdapterSessionLifecycle(siteSession, adapterSession);
432
+ const session = resolveAdapterBrowserSession(cmd, siteSession, adapterSession);
428
433
  const keepTab = resolveKeepTab(siteSession, opts.keepTab);
429
434
  const windowMode = resolveBrowserWindowMode('background', opts.windowMode);
430
- result = await browserSession(BrowserFactory, async (page) => {
431
- const observation = traceMode === 'off'
432
- ? null
433
- : new ObservationSession({
434
- scope: {
435
- contextId,
436
- session,
437
- target: page.getActivePage?.(),
438
- site: cmd.site,
439
- command: fullName(cmd),
440
- adapterSourcePath: resolveAdapterSourcePath(internal)
441
- ?? resolveAdapterSourcePath(initialTraceCommand),
442
- },
443
- });
444
- if (observation) {
445
- observation.record({
446
- stream: 'action',
447
- name: 'command',
448
- phase: 'start',
449
- data: { args: kwargs },
450
- });
451
- await page.startNetworkCapture?.().catch(() => false);
452
- }
453
- const preNavUrl = resolvePreNav(cmd);
454
- if (preNavUrl && await shouldRunPreNav(cmd, page, siteSession, preNavUrl)) {
455
- observation?.record({
456
- stream: 'action',
457
- name: 'pre_navigate',
458
- phase: 'start',
459
- data: { url: preNavUrl },
460
- });
461
- // Navigate directly — the extension's handleNavigate already has a fast-path
462
- // that skips navigation if the tab is already at the target URL.
463
- // This avoids an extra exec round-trip (getCurrentUrl) on first command and
464
- // lets the extension create the automation window with the target URL directly
465
- // instead of about:blank.
466
- try {
467
- await page.goto(preNavUrl);
468
- observation?.record({
435
+ const executeInBrowser = () => browserSession(BrowserFactory, async (page) => {
436
+ // BrowserBridge.connect() has started/probed the daemon by this point. Resolve
437
+ // the daemon's canonical profile before acquiring a profile-scoped lease.
438
+ const leaseContextId = adapterSession
439
+ ? await resolveAdapterLeaseContextId(contextId)
440
+ : undefined;
441
+ if (leaseContextId)
442
+ page.setContextId?.(leaseContextId);
443
+ const executeOnPage = async () => {
444
+ const observation = traceMode === 'off'
445
+ ? null
446
+ : new ObservationSession({
447
+ scope: {
448
+ contextId,
449
+ session,
450
+ target: page.getActivePage?.(),
451
+ site: cmd.site,
452
+ command: fullName(cmd),
453
+ adapterSourcePath: resolveAdapterSourcePath(internal)
454
+ ?? resolveAdapterSourcePath(initialTraceCommand),
455
+ },
456
+ });
457
+ if (observation) {
458
+ observation.record({
469
459
  stream: 'action',
470
- name: 'pre_navigate',
471
- phase: 'end',
472
- data: { url: preNavUrl },
460
+ name: 'command',
461
+ phase: 'start',
462
+ data: { args: kwargs },
473
463
  });
464
+ await page.startNetworkCapture?.().catch(() => false);
474
465
  }
475
- catch (err) {
466
+ const preNavUrl = resolvePreNav(cmd);
467
+ if (preNavUrl && await shouldRunPreNav(cmd, page, siteSession, preNavUrl)) {
476
468
  observation?.record({
477
469
  stream: 'action',
478
470
  name: 'pre_navigate',
479
- phase: 'error',
480
- data: { url: preNavUrl, error: err instanceof Error ? err.message : String(err) },
471
+ phase: 'start',
472
+ data: { url: preNavUrl },
481
473
  });
482
- const wrapped = new CommandExecutionError(`Pre-navigation to ${preNavUrl} failed: ${err instanceof Error ? err.message : err}`, 'Check that the site is reachable and the browser extension is running.');
483
- if (observation && (traceMode === 'on' || traceMode === 'retain-on-failure')) {
484
- observation.record({
485
- stream: 'error',
486
- message: wrapped.message,
487
- stack: wrapped.stack,
488
- code: wrapped.code,
489
- hint: wrapped.hint,
474
+ // Navigate directly the extension's handleNavigate already has a fast-path
475
+ // that skips navigation if the tab is already at the target URL.
476
+ // This avoids an extra exec round-trip (getCurrentUrl) on first command and
477
+ // lets the extension create the automation window with the target URL directly
478
+ // instead of about:blank.
479
+ try {
480
+ await page.goto(preNavUrl);
481
+ observation?.record({
482
+ stream: 'action',
483
+ name: 'pre_navigate',
484
+ phase: 'end',
485
+ data: { url: preNavUrl },
490
486
  });
491
- await collectObservationEvidence(observation, page).catch(() => { });
492
- exportTraceArtifact(observation, 'failure', wrapped, opts.onTraceExport);
493
487
  }
494
- throw wrapped;
495
- }
496
- }
497
- try {
498
- const browserTimeout = userTimeoutSec !== null
499
- ? userTimeoutSec + RUNTIME_TIMEOUT_PADDING_SECONDS
500
- : DEFAULT_BROWSER_COMMAND_TIMEOUT;
501
- const result = await runWithTimeout(runCommand(cmd, page, kwargs, debug), {
502
- timeout: browserTimeout,
503
- label: fullName(cmd),
504
- });
505
- observation?.record({
506
- stream: 'action',
507
- name: 'command',
508
- phase: 'end',
509
- });
510
- if (observation && traceMode === 'on') {
511
- await collectObservationEvidence(observation, page).catch(() => { });
512
- exportTraceArtifact(observation, 'success', undefined, opts.onTraceExport);
488
+ catch (err) {
489
+ observation?.record({
490
+ stream: 'action',
491
+ name: 'pre_navigate',
492
+ phase: 'error',
493
+ data: { url: preNavUrl, error: err instanceof Error ? err.message : String(err) },
494
+ });
495
+ const wrapped = new CommandExecutionError(`Pre-navigation to ${preNavUrl} failed: ${err instanceof Error ? err.message : err}`, 'Check that the site is reachable and the browser extension is running.');
496
+ if (observation && (traceMode === 'on' || traceMode === 'retain-on-failure')) {
497
+ observation.record({
498
+ stream: 'error',
499
+ message: wrapped.message,
500
+ stack: wrapped.stack,
501
+ code: wrapped.code,
502
+ hint: wrapped.hint,
503
+ });
504
+ await collectObservationEvidence(observation, page).catch(() => { });
505
+ exportTraceArtifact(observation, 'failure', wrapped, opts.onTraceExport);
506
+ }
507
+ throw wrapped;
508
+ }
513
509
  }
514
- // Adapter commands are one-shot — release the current tab lease immediately
515
- // instead of waiting for the 30s idle timeout. The automation container
516
- // window stays open for reuse.
517
- if (!keepTab)
518
- await page.closeWindow?.().catch(() => { });
519
- return result;
520
- }
521
- catch (err) {
522
- if (observation) {
523
- observation.record({
510
+ try {
511
+ const browserTimeout = userTimeoutSec !== null
512
+ ? userTimeoutSec + RUNTIME_TIMEOUT_PADDING_SECONDS
513
+ : DEFAULT_BROWSER_COMMAND_TIMEOUT;
514
+ const commandOperation = runCommand(cmd, page, kwargs, debug);
515
+ const result = adapterSession
516
+ ? await settleAdapterOperationAfterTimeout(commandOperation, browserTimeout * 1_000, new TimeoutError(fullName(cmd), browserTimeout), async () => { await page.closeWindow?.().catch(() => { }); })
517
+ : await runWithTimeout(commandOperation, {
518
+ timeout: browserTimeout,
519
+ label: fullName(cmd),
520
+ });
521
+ observation?.record({
524
522
  stream: 'action',
525
523
  name: 'command',
526
- phase: 'error',
527
- data: { error: err instanceof Error ? err.message : String(err) },
528
- });
529
- observation.record({
530
- stream: 'error',
531
- message: err instanceof Error ? err.message : String(err),
532
- stack: err instanceof Error ? err.stack : undefined,
524
+ phase: 'end',
533
525
  });
534
- if (traceMode === 'on' || traceMode === 'retain-on-failure') {
526
+ if (observation && traceMode === 'on') {
535
527
  await collectObservationEvidence(observation, page).catch(() => { });
536
- exportTraceArtifact(observation, 'failure', err, opts.onTraceExport);
528
+ exportTraceArtifact(observation, 'success', undefined, opts.onTraceExport);
537
529
  }
530
+ // Adapter commands are one-shot — release the current tab lease immediately
531
+ // instead of waiting for the 30s idle timeout. The automation container
532
+ // window stays open for reuse.
533
+ if (!keepTab)
534
+ await page.closeWindow?.().catch(() => { });
535
+ return result;
538
536
  }
539
- // Release the tab lease on failure too — without this, the lease lingers
540
- // until the extension's idle timer fires (unreliable on Windows where
541
- // MV3 service workers may be suspended before setTimeout triggers).
542
- if (!keepTab)
543
- await page.closeWindow?.().catch(() => { });
544
- throw err;
545
- }
537
+ catch (err) {
538
+ if (observation) {
539
+ observation.record({
540
+ stream: 'action',
541
+ name: 'command',
542
+ phase: 'error',
543
+ data: { error: err instanceof Error ? err.message : String(err) },
544
+ });
545
+ observation.record({
546
+ stream: 'error',
547
+ message: err instanceof Error ? err.message : String(err),
548
+ stack: err instanceof Error ? err.stack : undefined,
549
+ });
550
+ if (traceMode === 'on' || traceMode === 'retain-on-failure') {
551
+ await collectObservationEvidence(observation, page).catch(() => { });
552
+ exportTraceArtifact(observation, 'failure', err, opts.onTraceExport);
553
+ }
554
+ }
555
+ // Release the tab lease on failure too — without this, the lease lingers
556
+ // until the extension's idle timer fires (unreliable on Windows where
557
+ // MV3 service workers may be suspended before setTimeout triggers).
558
+ if (!keepTab)
559
+ await page.closeWindow?.().catch(() => { });
560
+ throw err;
561
+ }
562
+ };
563
+ if (!adapterSession)
564
+ return executeOnPage();
565
+ return withAdapterCommandLease({
566
+ requestId: crypto.randomUUID(),
567
+ contextId: leaseContextId,
568
+ surface: 'adapter',
569
+ site: cmd.site,
570
+ adapterSession,
571
+ sessionKey: session,
572
+ queueTimeoutMs: (adapterQueueTimeoutSeconds ?? 300) * 1_000,
573
+ maxParallel: cmd.adapterConcurrency?.maxParallel ?? 1,
574
+ }, executeOnPage, {
575
+ onLeaseLost: async () => { await page.closeWindow?.().catch(() => { }); },
576
+ });
546
577
  }, { session, cdpEndpoint, contextId, windowMode, surface: 'adapter', siteSession });
578
+ result = await executeInBrowser();
547
579
  }
548
580
  else {
549
581
  // Non-browser commands: enforce a timeout only when the command exposes
@@ -674,9 +706,40 @@ function normalizeSiteSession(raw) {
674
706
  function resolveSiteSession(cmd, rawOption) {
675
707
  return normalizeSiteSession(rawOption) ?? cmd.siteSession ?? 'ephemeral';
676
708
  }
677
- function resolveAdapterBrowserSession(cmd, siteSession) {
678
- if (siteSession === 'persistent')
679
- return `site:${cmd.site}`;
709
+ function normalizeAdapterSession(cmd, resolvedBrowser, rawOption) {
710
+ if (rawOption === undefined || rawOption === null || rawOption === '')
711
+ return undefined;
712
+ if (!resolvedBrowser || cmd.adapterConcurrency?.isolatedTabs !== true) {
713
+ throw new AdapterCoordinationError('ADAPTER_SESSION_NOT_SUPPORTED', `${fullName(cmd)} does not support named Adapter sessions`);
714
+ }
715
+ const value = String(rawOption);
716
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value)) {
717
+ throw new AdapterCoordinationError('INVALID_ADAPTER_SESSION', '--adapter-session must match [A-Za-z0-9][A-Za-z0-9_-]{0,63}');
718
+ }
719
+ return value;
720
+ }
721
+ function normalizeAdapterQueueTimeout(rawOption, adapterSession) {
722
+ if (rawOption === undefined || rawOption === null || rawOption === '') {
723
+ return adapterSession ? 300 : undefined;
724
+ }
725
+ if (!adapterSession) {
726
+ throw new AdapterCoordinationError('INVALID_ADAPTER_QUEUE_TIMEOUT', '--adapter-queue-timeout requires --adapter-session');
727
+ }
728
+ const value = typeof rawOption === 'string' && /^\d+$/.test(rawOption) ? Number(rawOption) : NaN;
729
+ if (!Number.isInteger(value) || value < 1 || value > 3600) {
730
+ throw new AdapterCoordinationError('INVALID_ADAPTER_QUEUE_TIMEOUT', '--adapter-queue-timeout must be an integer between 1 and 3600 seconds');
731
+ }
732
+ return value;
733
+ }
734
+ function assertAdapterSessionLifecycle(siteSession, adapterSession) {
735
+ if (adapterSession && siteSession !== 'persistent') {
736
+ throw new AdapterCoordinationError('ADAPTER_SESSION_REQUIRES_PERSISTENT', '--adapter-session requires --site-session persistent');
737
+ }
738
+ }
739
+ function resolveAdapterBrowserSession(cmd, siteSession, adapterSession) {
740
+ if (siteSession === 'persistent') {
741
+ return adapterSession ? `site:${cmd.site}:${adapterSession}` : `site:${cmd.site}`;
742
+ }
680
743
  return `site:${cmd.site}:${crypto.randomUUID()}`;
681
744
  }
682
745
  function normalizeBooleanOption(name, raw) {
@@ -82,6 +82,7 @@ export declare function siteHelpData(site: string, commands: readonly CliCommand
82
82
  export declare function commandHelpData(cmd: CliCommand): Record<string, unknown>;
83
83
  export declare function formatCommonOptionsHelpText(): string;
84
84
  export declare function formatBrowserCommonOptionsHelpText(): string;
85
+ export declare function formatAdapterSessionOptionsHelpText(): string;
85
86
  export declare function formatSiteHelpText(site: string, commands: readonly CliCommand[]): string;
86
87
  export declare function formatCommandHelpText(cmd: CliCommand): string;
87
88
  export declare function installStructuredHelp(command: Command, data: () => unknown, textSuffix?: string | (() => string)): void;
package/dist/src/help.js CHANGED
@@ -48,6 +48,19 @@ const BROWSER_COMMON_OPTIONS = [
48
48
  choices: ['true', 'false'],
49
49
  },
50
50
  ];
51
+ const ADAPTER_SESSION_OPTIONS = [
52
+ {
53
+ flags: '--adapter-session <name>',
54
+ name: 'adapter-session',
55
+ help: 'Named persistent Adapter tab session',
56
+ },
57
+ {
58
+ flags: '--adapter-queue-timeout <seconds>',
59
+ name: 'adapter-queue-timeout',
60
+ help: 'Seconds to wait for an Adapter command lease',
61
+ default: 300,
62
+ },
63
+ ];
51
64
  function normalizeStructuredHelpFormat(value) {
52
65
  const normalized = value?.toLowerCase();
53
66
  if (normalized === 'yaml' || normalized === 'yml')
@@ -369,6 +382,12 @@ function compactCommand(cmd) {
369
382
  positionals: positionals(cmd).map(compactArg),
370
383
  command_options: commandOptions(cmd).map(compactArg),
371
384
  ...(hasBrowserCapability(cmd) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
385
+ ...(cmd.adapterConcurrency?.isolatedTabs === true
386
+ ? {
387
+ adapterConcurrency: cmd.adapterConcurrency,
388
+ adapter_common_options: ADAPTER_SESSION_OPTIONS.map(compactCommonOption),
389
+ }
390
+ : {}),
372
391
  example: formatCommandExample(cmd),
373
392
  ...(cmd.siteSession ? { siteSession: cmd.siteSession } : {}),
374
393
  ...(cmd.defaultFormat ? { defaultFormat: cmd.defaultFormat } : {}),
@@ -417,6 +436,9 @@ export function siteHelpData(site, commands) {
417
436
  commands: unique.map(cmd => compactCommand(cmd)),
418
437
  common_options: COMMON_OPTIONS.map(compactCommonOption),
419
438
  ...(unique.some(hasBrowserCapability) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
439
+ ...(unique.some(cmd => cmd.adapterConcurrency?.isolatedTabs === true)
440
+ ? { adapter_common_options: ADAPTER_SESSION_OPTIONS.map(compactCommonOption) }
441
+ : {}),
420
442
  next: [
421
443
  `bycli ${site} <command> --help -f yaml`,
422
444
  `bycli ${site} <command> -f yaml`,
@@ -429,6 +451,12 @@ export function commandHelpData(cmd) {
429
451
  ...compactCommand(cmd),
430
452
  common_options: COMMON_OPTIONS.map(compactCommonOption),
431
453
  ...(hasBrowserCapability(cmd) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
454
+ ...(cmd.adapterConcurrency?.isolatedTabs === true
455
+ ? {
456
+ adapterConcurrency: cmd.adapterConcurrency,
457
+ adapter_common_options: ADAPTER_SESSION_OPTIONS.map(compactCommonOption),
458
+ }
459
+ : {}),
432
460
  output_formats: ['table', 'plain', 'yaml', 'json', 'md', 'csv'],
433
461
  };
434
462
  }
@@ -468,6 +496,15 @@ export function formatBrowserCommonOptionsHelpText() {
468
496
  });
469
497
  return ['Browser common options:', ...formatRows(rows)].join('\n');
470
498
  }
499
+ export function formatAdapterSessionOptionsHelpText() {
500
+ const rows = ADAPTER_SESSION_OPTIONS.map(option => {
501
+ const details = [option.help];
502
+ if ('default' in option)
503
+ details.push(`default: ${option.default}`);
504
+ return [option.flags, details.join(' ')];
505
+ });
506
+ return ['Adapter session options:', ...formatRows(rows)].join('\n');
507
+ }
471
508
  export function formatSiteHelpText(site, commands) {
472
509
  const unique = [...new Map(commands.map(cmd => [fullName(cmd), cmd])).values()]
473
510
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -481,6 +518,7 @@ export function formatSiteHelpText(site, commands) {
481
518
  '',
482
519
  formatCommonOptionsHelpText(),
483
520
  ...(unique.some(hasBrowserCapability) ? ['', formatBrowserCommonOptionsHelpText()] : []),
521
+ ...(unique.some(cmd => cmd.adapterConcurrency?.isolatedTabs === true) ? ['', formatAdapterSessionOptionsHelpText()] : []),
484
522
  '',
485
523
  `Agent tip: use 'bycli ${site} --help -f yaml' to get all command args/options in one structured response.`,
486
524
  '',
@@ -511,6 +549,8 @@ export function formatCommandHelpText(cmd) {
511
549
  lines.push(formatCommonOptionsHelpText(), '');
512
550
  if (hasBrowserCapability(cmd))
513
551
  lines.push(formatBrowserCommonOptionsHelpText(), '');
552
+ if (cmd.adapterConcurrency?.isolatedTabs === true)
553
+ lines.push(formatAdapterSessionOptionsHelpText(), '');
514
554
  const meta = [];
515
555
  meta.push(`Access: ${cmd.access}`);
516
556
  meta.push(`Browser: ${browserRequirementLabel(cmd)}`);
@@ -38,4 +38,8 @@ export interface ManifestEntry {
38
38
  navigateBefore?: boolean | string;
39
39
  /** Site session lifecycle defaults — see CliCommand.siteSession */
40
40
  siteSession?: 'ephemeral' | 'persistent';
41
+ adapterConcurrency?: {
42
+ isolatedTabs: true;
43
+ maxParallel: number;
44
+ };
41
45
  }
@@ -28,6 +28,10 @@ export type ConditionalBrowserCommandFunc = (page: IPage | null, kwargs: Command
28
28
  export type NonBrowserCommandFunc = (kwargs: CommandArgs, debug?: boolean) => Promise<unknown>;
29
29
  export type CommandAccess = 'read' | 'write';
30
30
  export type SiteSessionMode = 'ephemeral' | 'persistent';
31
+ export interface AdapterConcurrency {
32
+ isolatedTabs: true;
33
+ maxParallel: number;
34
+ }
31
35
  /**
32
36
  * 所有已注册 adapter command 的共享元数据和运行选项。
33
37
  *
@@ -82,6 +86,8 @@ interface BaseCliCommand {
82
86
  navigateBefore?: boolean | string;
83
87
  /** adapter 浏览器站点 session 生命周期:临时 session 或持久 session。 */
84
88
  siteSession?: SiteSessionMode;
89
+ /** Opt-in authorization for isolated persistent Adapter tabs. */
90
+ adapterConcurrency?: AdapterConcurrency;
85
91
  /** 用户没有传 `-f/--format` 时使用的默认输出格式。 */
86
92
  defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
87
93
  }
@@ -95,6 +95,7 @@ function rawCommandBase(opts) {
95
95
  validateArgs: opts.validateArgs,
96
96
  navigateBefore: opts.navigateBefore,
97
97
  siteSession: opts.siteSession,
98
+ adapterConcurrency: opts.adapterConcurrency,
98
99
  defaultFormat: opts.defaultFormat,
99
100
  };
100
101
  }
@@ -190,6 +191,27 @@ function assertSiteSession(cmd) {
190
191
  throw new Error(`Command ${key} siteSession must be one of: ephemeral, persistent`);
191
192
  }
192
193
  }
194
+ function assertAdapterConcurrency(cmd) {
195
+ if (cmd.adapterConcurrency === undefined)
196
+ return;
197
+ const key = `${cmd.site}/${cmd.name}`;
198
+ if (!cmd.adapterConcurrency || typeof cmd.adapterConcurrency !== 'object') {
199
+ throw new Error(`Command ${key} adapterConcurrency must be an object`);
200
+ }
201
+ const metadata = cmd.adapterConcurrency;
202
+ if (metadata.isolatedTabs !== true) {
203
+ throw new Error(`Command ${key} adapterConcurrency.isolatedTabs must be true`);
204
+ }
205
+ if (!Number.isInteger(metadata.maxParallel)
206
+ || metadata.maxParallel < 1
207
+ || metadata.maxParallel > 3) {
208
+ throw new Error(`Command ${key} adapterConcurrency.maxParallel must be an integer between 1 and 3`);
209
+ }
210
+ if (cmd.browser === false
211
+ || (cmd.browser === undefined && (cmd.strategy === Strategy.PUBLIC || cmd.strategy === Strategy.LOCAL))) {
212
+ throw new Error(`Command ${key} adapterConcurrency requires browser capability`);
213
+ }
214
+ }
193
215
  export function registerCommand(cmd) {
194
216
  registerCommandInput(cmd);
195
217
  }
@@ -197,6 +219,7 @@ function registerCommandInput(cmd) {
197
219
  withRegistryMutationGroup(() => {
198
220
  assertCommandAccess(cmd);
199
221
  assertSiteSession(cmd);
222
+ assertAdapterConcurrency(cmd);
200
223
  if (cmd.browser === 'conditional') {
201
224
  if (typeof cmd.requiresBrowser !== 'function') {
202
225
  const key = `${cmd.site}/${cmd.name}`;
@@ -33,6 +33,7 @@ export declare function serializeCommand(cmd: CliCommand): {
33
33
  example: string;
34
34
  defaultFormat: "table" | "plain" | "json" | "yaml" | "yml" | "md" | "markdown" | "csv" | null;
35
35
  siteSession: import("./registry.js").SiteSessionMode | null;
36
+ adapterConcurrency: import("./registry.js").AdapterConcurrency | null;
36
37
  };
37
38
  /** Human-readable arg summary: `<required> [optional]` style. */
38
39
  export declare function formatArgSummary(args: Arg[]): string;
@@ -35,6 +35,7 @@ export function serializeCommand(cmd) {
35
35
  example: formatCommandExample(cmd),
36
36
  defaultFormat: cmd.defaultFormat ?? null,
37
37
  siteSession: cmd.siteSession ?? null,
38
+ adapterConcurrency: cmd.adapterConcurrency ?? null,
38
39
  };
39
40
  }
40
41
  // ── Formatting ──────────────────────────────────────────────────────────────
@@ -222,6 +222,8 @@ export interface IPage {
222
222
  /** Focus the browser window containing the active page for interactive login. */
223
223
  focusWindow?(): Promise<void>;
224
224
  closeWindow?(): Promise<void>;
225
+ /** Pin subsequent commands to the daemon profile selected for this operation. */
226
+ setContextId?(contextId: string): void;
225
227
  /** Returns the current page URL, or null if unavailable. */
226
228
  getCurrentUrl?(): Promise<string | null>;
227
229
  /** Returns the active page identity (targetId), or undefined if not yet resolved. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovovs/bycli",
3
- "version": "2.1.39",
3
+ "version": "2.1.41",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,7 +33,8 @@
33
33
  "./download/media-download": "./dist/src/download/media-download.js",
34
34
  "./download/progress": "./dist/src/download/progress.js",
35
35
  "./download/wechat-article": "./dist/src/download/wechat-article.js",
36
- "./pipeline": "./dist/src/pipeline/index.js"
36
+ "./pipeline": "./dist/src/pipeline/index.js",
37
+ "./adapter-coordination": "./dist/src/adapter-coordination.js"
37
38
  },
38
39
  "files": [
39
40
  "dist/src/",