@mearl/client 2.17.1 → 2.19.0

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/dist/check.js CHANGED
@@ -1,4 +1,45 @@
1
1
  import { UnifiedClient, normalizeBrowserSelector, } from './unified.js';
2
+ const UPDATE_SOURCES = {
3
+ client: 'https://registry.npmjs.org/@mearl%2Fclient/latest',
4
+ 'native-host': 'https://registry.npmjs.org/@mearl%2Fnative-host/latest',
5
+ 'cloud-connector': 'https://registry.npmjs.org/@mearl%2Fcloud-connector/latest',
6
+ extension: 'https://mearl.alibaba-inc.com/config.json',
7
+ };
8
+ export function parseReleaseVersion(value) {
9
+ if (typeof value !== 'string')
10
+ return null;
11
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-beta\.(0|[1-9]\d*))?$/.exec(value);
12
+ if (!match)
13
+ return null;
14
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4] ?? 0)];
15
+ if (!parts.every(Number.isSafeInteger))
16
+ return null;
17
+ return [...parts.slice(0, 3), match[4] === undefined ? 1 : 0, parts[3]];
18
+ }
19
+ export function isNewerRelease(latest, installed) {
20
+ const latestParts = parseReleaseVersion(latest);
21
+ const installedParts = parseReleaseVersion(installed);
22
+ if (!latestParts || !installedParts)
23
+ return false;
24
+ for (let index = 0; index < latestParts.length; index++) {
25
+ if (latestParts[index] !== installedParts[index]) {
26
+ return latestParts[index] > installedParts[index];
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+ async function fetchLatestVersion(component) {
32
+ const response = await fetch(UPDATE_SOURCES[component], {
33
+ signal: AbortSignal.timeout(1500),
34
+ });
35
+ if (!response.ok)
36
+ throw new Error(`HTTP ${response.status}`);
37
+ const payload = (await response.json());
38
+ const version = component === 'extension' ? payload.latestVersion : payload.version;
39
+ if (!parseReleaseVersion(version))
40
+ throw new Error('Invalid release version');
41
+ return version;
42
+ }
2
43
  function formatError(error) {
3
44
  return error instanceof Error ? error.message : String(error);
4
45
  }
@@ -98,6 +139,9 @@ export async function runUnifiedCheck(options, dependencies = {}) {
98
139
  connectTimeoutSec: timeoutSec,
99
140
  requestTimeoutSec: timeoutSec,
100
141
  });
142
+ const latestVersions = options.checkUpdates
143
+ ? Promise.allSettled(Object.keys(UPDATE_SOURCES).map(component => (dependencies.fetchLatestVersion ?? fetchLatestVersion)(component)))
144
+ : null;
101
145
  const environment = [
102
146
  { status: 'ok', label: `Unified client v${options.clientVersion}` },
103
147
  ];
@@ -301,6 +345,67 @@ export async function runUnifiedCheck(options, dependencies = {}) {
301
345
  });
302
346
  }
303
347
  }));
348
+ let updates;
349
+ if (latestVersions) {
350
+ const latestResults = await latestVersions;
351
+ const components = Object.keys(UPDATE_SOURCES);
352
+ const installed = new Map();
353
+ const addInstalled = (component, version) => {
354
+ if (!version)
355
+ return;
356
+ const versions = installed.get(component) ?? new Set();
357
+ versions.add(version);
358
+ installed.set(component, versions);
359
+ };
360
+ addInstalled('client', options.clientVersion);
361
+ for (const check of browserChecks) {
362
+ addInstalled('native-host', check.versions?.nativeHost);
363
+ addInstalled('extension', check.versions?.extension);
364
+ }
365
+ for (const connector of connectors)
366
+ addInstalled('cloud-connector', connector.version);
367
+ updates = components.flatMap((component, index) => {
368
+ const result = latestResults[index];
369
+ return [...(installed.get(component) ?? [])].map(version => ({
370
+ component,
371
+ installed: version,
372
+ ...(result.status === 'fulfilled' ? { latest: result.value } : {}),
373
+ status: result.status === 'rejected' ||
374
+ !parseReleaseVersion(version) ||
375
+ !parseReleaseVersion(result.value)
376
+ ? 'unavailable'
377
+ : isNewerRelease(result.value, version)
378
+ ? 'available'
379
+ : 'current',
380
+ }));
381
+ });
382
+ const available = updates.filter(update => update.status === 'available');
383
+ const unavailable = updates.filter(update => update.status === 'unavailable');
384
+ const packageUpdateAvailable = available.some(update => update.component === 'client' || update.component === 'native-host');
385
+ const extensionUpdateAvailable = available.some(update => update.component === 'extension');
386
+ const connectorUpdateAvailable = available.some(update => update.component === 'cloud-connector');
387
+ environment.push({
388
+ status: available.length || unavailable.length ? 'warning' : 'ok',
389
+ label: available.length
390
+ ? `${available.length} Mearl update${available.length === 1 ? '' : 's'} available`
391
+ : unavailable.length
392
+ ? 'Mearl update check incomplete'
393
+ : 'Mearl packages are current',
394
+ details: [
395
+ ...available.map(update => `${update.component}: ${update.installed} → ${update.latest}`),
396
+ ...(packageUpdateAvailable
397
+ ? ['Update on the affected machine: npx @mearl/setup update']
398
+ : []),
399
+ ...(extensionUpdateAvailable
400
+ ? ['Update a setup-managed extension: npx @mearl/setup extension']
401
+ : []),
402
+ ...(connectorUpdateAvailable
403
+ ? ['Update cloud-connector with its package manager on the connector machine.']
404
+ : []),
405
+ ...unavailable.map(update => `${update.component}: latest version unavailable`),
406
+ ],
407
+ });
408
+ }
304
409
  const hasError = environment.some(item => item.status === 'error') ||
305
410
  browserChecks.some(check => check.item.status === 'error');
306
411
  const listedBrowsers = browserList?.browsers ?? [];
@@ -336,6 +441,7 @@ export async function runUnifiedCheck(options, dependencies = {}) {
336
441
  connectors,
337
442
  environment,
338
443
  ok: !hasError,
444
+ ...(updates ? { updates } : {}),
339
445
  };
340
446
  writeJson(JSON.stringify(result, null, 2));
341
447
  return result;
package/dist/cli.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface ParsedArgs {
10
10
  forceCdp: boolean;
11
11
  json: boolean;
12
12
  local: boolean;
13
+ updates: boolean;
13
14
  outputPath?: string;
14
15
  payload: Record<string, any>;
15
16
  serverUrl?: string;
package/dist/cli.js CHANGED
@@ -77,6 +77,7 @@ const PLUGIN_HOST_OPTIONS = new Set([
77
77
  '--help',
78
78
  '--json',
79
79
  '--local',
80
+ '--updates',
80
81
  '--output',
81
82
  '--payload',
82
83
  '--payload-file',
@@ -266,6 +267,7 @@ function printCheckHelp(commandName) {
266
267
  ' --server <url> 覆盖 cloud-server WebSocket URL',
267
268
  ' --timeout <seconds> 单项检查超时时间,默认 5 秒',
268
269
  ' --json 输出机器可读 JSON',
270
+ ' --updates 查询已安装组件的发布版本',
269
271
  ' -h, --help 查看帮助',
270
272
  ].join('\n'));
271
273
  }
@@ -409,6 +411,7 @@ export function parseClientArgs(args, plugin = false) {
409
411
  .option('--browser <id-or-name>')
410
412
  .option('--connector <id-or-name>')
411
413
  .option('--local')
414
+ .option('--updates')
412
415
  .option('--cdp')
413
416
  .option('--server <url>');
414
417
  if (plugin)
@@ -442,6 +445,8 @@ export function parseClientArgs(args, plugin = false) {
442
445
  const json = options.json === true;
443
446
  if (json && action !== 'check')
444
447
  throw new Error('--json 仅适用于 check');
448
+ if (options.updates && action !== 'check')
449
+ throw new Error('--updates 仅适用于 check');
445
450
  const parsedPayload = parsePayload(options.payload, options.payloadFile);
446
451
  const { payload, outputPath } = normalizeScreenshotOutput(action, parsedPayload, options.output);
447
452
  const fallbackTimeout = resolveActionTimeoutSec(action, payload, 60);
@@ -459,6 +464,7 @@ export function parseClientArgs(args, plugin = false) {
459
464
  forceCdp,
460
465
  json,
461
466
  local,
467
+ updates: options.updates === true,
462
468
  ...(outputPath ? { outputPath } : {}),
463
469
  ...(options.browser ? { browser: options.browser } : {}),
464
470
  ...(connector ? { connector } : {}),
@@ -504,7 +510,8 @@ async function runPluginCli(args, commandName) {
504
510
  ...invokeOptions,
505
511
  ...(parsed.verbose
506
512
  ? {
507
- onProgress(message) {
513
+ onProgress(progress) {
514
+ const message = typeof progress === 'string' ? progress : progress.message;
508
515
  process.stderr.write(message.endsWith('\n') ? message : `${message}\n`);
509
516
  },
510
517
  }
@@ -644,6 +651,7 @@ export async function runMearlCli(args, options = {}) {
644
651
  ...(parsed.connector ? { connector: parsed.connector } : {}),
645
652
  ...(parsed.local ? { local: true } : {}),
646
653
  ...(parsed.forceCdp ? { transport: 'cdp' } : {}),
654
+ ...(parsed.updates ? { checkUpdates: true } : {}),
647
655
  };
648
656
  if (parsed.json) {
649
657
  const result = await runUnifiedCheck({ ...checkOptions, json: true });
@@ -348,7 +348,7 @@ export interface PageScreenshotParams {
348
348
  format?: 'png' | 'jpeg';
349
349
  quality?: number;
350
350
  }
351
- export declare const BROWSER_DEVICE_PRESET_NAMES: readonly ["iphone-17-pro", "iphone-17e", "pixel-11", "ipad-mini-a17-pro", "galaxy-s26", "pura-x-max", "iphone-15-pro", "iphone-se", "pixel-8", "ipad-mini", "galaxy-s23"];
351
+ export declare const BROWSER_DEVICE_PRESET_NAMES: readonly ["iphone-duo-folded", "iphone-duo-unfolded", "iphone-17-pro", "iphone-17e", "pixel-11", "ipad-mini-a17-pro", "galaxy-s26", "pura-x-max", "iphone-15-pro", "iphone-se", "pixel-8", "ipad-mini", "galaxy-s23"];
352
352
  export type BrowserDevicePresetName = (typeof BROWSER_DEVICE_PRESET_NAMES)[number];
353
353
  export interface BrowserEmulationOptions {
354
354
  preset?: BrowserDevicePresetName;
@@ -365,7 +365,7 @@ export interface BrowserEmulationOptions {
365
365
  /** App 容器横屏时将根页面与业务页面分置左右 WebView;仅与 deviceShell、App 档案同时生效。 */
366
366
  splitScreen?: boolean;
367
367
  }
368
- export type AppBridgeConfigBehavior = 'navigate' | 'closeAll' | 'closeBackData' | 'postMessage' | 'notifySubscribe' | 'notifyUnsubscribe' | 'notifyPost' | 'sharedFileRead' | 'sharedFileWrite' | 'sharedDataGet' | 'sharedDataSet' | 'legacyBroadcastCreate' | 'legacyBroadcastPost' | 'legacyBroadcastClose' | 'memoryGet' | 'memorySet' | 'memoryDelete' | 'availabilityMap' | 'userInfo' | 'networkInfo' | 'geolocation' | 'permission' | 'prefetch' | 'fptTrack' | 'mtopStream' | 'previewFile' | 'toast' | 'unsupportedNativeInput' | 'payment' | 'userTrack' | 'calendar' | 'networkQualityMonitor' | 'beaconRequest' | 'voiceRecognition';
368
+ export type AppBridgeConfigBehavior = 'navigate' | 'closeAll' | 'closeBackData' | 'postMessage' | 'notifySubscribe' | 'notifyUnsubscribe' | 'notifyPost' | 'sharedFileRead' | 'sharedFileWrite' | 'sharedDataGet' | 'sharedDataSet' | 'legacyBroadcastCreate' | 'legacyBroadcastPost' | 'legacyBroadcastClose' | 'memoryGet' | 'memorySet' | 'memoryDelete' | 'availabilityMap' | 'userInfo' | 'authCode' | 'networkInfo' | 'geolocation' | 'permission' | 'imagePicker' | 'prefetch' | 'fptTrack' | 'mtopStream' | 'previewFile' | 'toast' | 'unsupportedNativeInput' | 'payment' | 'userTrack' | 'calendar' | 'networkQualityMonitor' | 'beaconRequest' | 'voiceRecognition';
369
369
  /** 单个桥方法的声明式覆盖;行为、固定回包、成功空实现三选一。 */
370
370
  export interface AppBridgeMethodConfig {
371
371
  behavior?: AppBridgeConfigBehavior;
@@ -421,6 +421,8 @@ export interface SetAppProfileParams {
421
421
  profile?: AppProfileName;
422
422
  os?: 'ios' | 'android';
423
423
  appVersion?: string;
424
+ /** 支付宝容器语言;其他容器忽略。 */
425
+ language?: 'zh-CN' | 'zh-TW' | 'zh-HK' | 'en-US';
424
426
  deviceModel?: string;
425
427
  /** 按方法覆盖档案内置桥配置,仅支持受控行为、固定 JSON 回包和成功空实现 */
426
428
  bridgeConfig?: AppBridgeConfig;
@@ -7,6 +7,8 @@ export const SEND_REQUEST_MAX_TOTAL_FILE_BYTES = 40 * 1024 * 1024;
7
7
  // the 10 MiB cloud message limit.
8
8
  export const SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES = 7 * 1024 * 1024;
9
9
  export const BROWSER_DEVICE_PRESET_NAMES = [
10
+ 'iphone-duo-folded',
11
+ 'iphone-duo-unfolded',
10
12
  'iphone-17-pro',
11
13
  'iphone-17e',
12
14
  'pixel-11',
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
8
8
  export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
9
- export type { PluginInvocation, PluginInvokeOptions } from './plugin.js';
9
+ export type { PluginInvocation, PluginInvokeOptions, PluginProgress, StructuredPluginProgress, } from './plugin.js';
10
10
  export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
11
11
  export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCapabilities, BrowserCapabilityAction, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserJsonValue, BrowserLaunchParams, BrowserProviderOperation, BrowserListResult, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserProviderDefinition, BrowserProviderIdentity, BrowserProviderInputSchema, BrowserProviderListResult, BrowserReference, BrowserDetails, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageInspectFilter, PageInspectParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from './generated-browser-action-protocol.js';
12
12
  import { type UnifiedInvokeOptions } from './unified.js';
package/dist/plugin.d.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  import { type OfficialPluginInstaller } from './officialPluginInstaller.js';
2
+ import { type PluginToolboxContribution } from './pluginRegistry.js';
2
3
  import type { UnifiedInvokeOptions } from './unified.js';
3
- export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, type PluginRegistration, type PluginRegistrationOptions, } from './pluginRegistry.js';
4
+ export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, type PluginRegistration, type PluginRegistrationOptions, type PluginToolboxAgentConfig, type PluginPageBundleRegistration, type PluginToolboxContribution, type PluginToolboxLocale, type PluginToolboxSandboxPage, } from './pluginRegistry.js';
4
5
  export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, type OfficialPluginDefinition, type OfficialPluginName, } from './officialPluginCatalog.js';
5
6
  export type PluginOptions = UnifiedInvokeOptions;
7
+ export interface StructuredPluginProgress {
8
+ message: string;
9
+ data?: Record<string, unknown>;
10
+ }
11
+ export type PluginProgress = string | StructuredPluginProgress;
6
12
  export interface PluginInvocation {
7
13
  command: string;
8
14
  input: Record<string, any>;
9
15
  }
10
16
  export interface PluginInvokeOptions extends UnifiedInvokeOptions {
11
- onProgress?: (message: string) => void;
17
+ onProgress?: (progress: PluginProgress) => void;
12
18
  }
13
19
  export interface PluginExecutionContext {
14
20
  ensurePlugin?: OfficialPluginInstaller;
@@ -18,13 +24,14 @@ export interface PluginApp {
18
24
  readonly options: Readonly<PluginOptions>;
19
25
  command(signature: string, handler: PluginHandler, description?: string): void;
20
26
  invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
21
- progress(message: string): void;
27
+ progress(progress: PluginProgress): void;
22
28
  }
23
29
  export type PluginHandler = (input: Record<string, any>) => unknown | Promise<unknown>;
24
30
  /** 注册和执行复用同一宿主能力边界;仅 handler 执行期间开放浏览器调用。 */
25
31
  export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invoke' | 'options' | 'progress'>, pluginName?: string): Promise<{
26
32
  name: string | undefined;
27
33
  description: string | undefined;
34
+ toolbox: PluginToolboxContribution | undefined;
28
35
  version: string | undefined;
29
36
  commandHelp(namespace: string): string;
30
37
  help(namespace: string, name?: string): string;
@@ -33,6 +40,7 @@ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invo
33
40
  export declare function loadRegisteredPlugin(name: string, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<{
34
41
  name: string | undefined;
35
42
  description: string | undefined;
43
+ toolbox: PluginToolboxContribution | undefined;
36
44
  version: string | undefined;
37
45
  commandHelp(namespace: string): string;
38
46
  help(namespace: string, name?: string): string;
package/dist/plugin.js CHANGED
@@ -5,9 +5,9 @@ import { Command } from 'commander';
5
5
  import { COMMAND_MAP } from './commands.js';
6
6
  import { findOfficialPlugin } from './officialPluginCatalog.js';
7
7
  import { ensureOfficialPluginRegistered, } from './officialPluginInstaller.js';
8
- import { findPluginRegistration, PluginError } from './pluginRegistry.js';
8
+ import { findPluginRegistration, normalizePluginToolboxContribution, PluginError, } from './pluginRegistry.js';
9
9
  import { describePythonPlugin, runPythonPluginCommand } from './pythonPlugin.js';
10
- export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, } from './pluginRegistry.js';
10
+ export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, readPluginPageBundleChunk, } from './pluginRegistry.js';
11
11
  export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, } from './officialPluginCatalog.js';
12
12
  function readPluginVersion(entry) {
13
13
  try {
@@ -26,6 +26,36 @@ function isObject(value) {
26
26
  function isPluginInvocation(value) {
27
27
  return isObject(value) && typeof value.command === 'string' && isObject(value.input);
28
28
  }
29
+ function normalizePluginProgress(progress) {
30
+ if (typeof progress === 'string') {
31
+ if (!progress)
32
+ throw new Error('app.progress 需要非空消息');
33
+ return progress;
34
+ }
35
+ if (!isObject(progress) || typeof progress.message !== 'string' || !progress.message) {
36
+ throw new Error('app.progress 需要非空消息');
37
+ }
38
+ if (progress.data !== undefined && !isObject(progress.data)) {
39
+ throw new Error('app.progress data 必须是对象');
40
+ }
41
+ let data;
42
+ if (progress.data) {
43
+ try {
44
+ const serialized = JSON.stringify(progress.data);
45
+ const parsed = serialized === undefined ? undefined : JSON.parse(serialized);
46
+ if (!isObject(parsed))
47
+ throw new Error('invalid object');
48
+ data = Object.freeze(parsed);
49
+ }
50
+ catch {
51
+ throw new Error('app.progress data 必须是可 JSON 序列化的对象');
52
+ }
53
+ }
54
+ return Object.freeze({
55
+ message: progress.message,
56
+ ...(data ? { data } : {}),
57
+ });
58
+ }
29
59
  function missingPluginEntryMessage(entry, pluginName) {
30
60
  const name = pluginName ?? '<name>';
31
61
  return [
@@ -54,6 +84,7 @@ export async function loadPlugin(entry, context, pluginName) {
54
84
  let registering = true;
55
85
  let name;
56
86
  let description;
87
+ let toolbox;
57
88
  const app = Object.freeze({
58
89
  options: Object.freeze({ ...context.options }),
59
90
  command(signature, handler, description = '') {
@@ -80,12 +111,10 @@ export async function loadPlugin(entry, context, pluginName) {
80
111
  throw new Error('app.invoke 只能在命令执行期间调用');
81
112
  return context.invoke(action, payload, options);
82
113
  },
83
- progress(message) {
114
+ progress(progress) {
84
115
  if (!executing)
85
116
  throw new Error('app.progress 只能在命令执行期间调用');
86
- if (typeof message !== 'string' || !message)
87
- throw new Error('app.progress 需要非空字符串');
88
- context.progress(message);
117
+ context.progress(normalizePluginProgress(progress));
89
118
  },
90
119
  });
91
120
  try {
@@ -93,6 +122,7 @@ export async function loadPlugin(entry, context, pluginName) {
93
122
  const descriptor = await describePythonPlugin(entry, app.options);
94
123
  name = descriptor.name;
95
124
  description = descriptor.description;
125
+ toolbox = normalizePluginToolboxContribution(descriptor.toolbox);
96
126
  for (const command of descriptor.commands) {
97
127
  const commandName = command.signature.trim().split(/\s+/)[0];
98
128
  app.command(command.signature, input => runPythonPluginCommand(entry, commandName, input, app), command.description);
@@ -108,12 +138,18 @@ export async function loadPlugin(entry, context, pluginName) {
108
138
  if (module.description !== undefined && typeof module.description !== 'string')
109
139
  throw new Error('入口的 description 导出必须为字符串');
110
140
  description = module.description;
141
+ toolbox = normalizePluginToolboxContribution(module.toolbox);
111
142
  if (typeof module.default !== 'function')
112
143
  throw new Error('入口必须默认导出 register(app) 函数');
113
144
  await module.default(app);
114
145
  }
115
146
  if (commands.size === 0)
116
147
  throw new Error('入口未注册任何命令');
148
+ const toolboxCommands = toolbox?.page.commands ?? [];
149
+ for (const command of toolboxCommands) {
150
+ if (!commands.has(command))
151
+ throw new Error(`toolbox 命令未注册: ${command}`);
152
+ }
117
153
  }
118
154
  catch (error) {
119
155
  throw new PluginError('PLUGIN_LOAD_FAILED', `${entry}: ${error instanceof Error ? error.message : String(error)}`);
@@ -130,6 +166,7 @@ export async function loadPlugin(entry, context, pluginName) {
130
166
  return {
131
167
  name,
132
168
  description,
169
+ toolbox,
133
170
  version: readPluginVersion(entry),
134
171
  commandHelp(namespace) {
135
172
  return [...commands.values()]
@@ -215,7 +252,7 @@ async function loadRegisteredPluginWithState(name, context, options = {}, state
215
252
  }
216
253
  let registration = findPluginRegistration(name);
217
254
  if (!registration && findOfficialPlugin(name)) {
218
- await (context.ensurePlugin ?? ensureOfficialPluginRegistered)(name, options.onProgress);
255
+ await (context.ensurePlugin ?? ensureOfficialPluginRegistered)(name, options.onProgress ? message => options.onProgress?.(message) : undefined);
219
256
  registration = findPluginRegistration(name);
220
257
  }
221
258
  if (!registration) {
@@ -0,0 +1,8 @@
1
+ export declare const PLUGIN_PAGE_CHUNK_BYTES: number;
2
+ export interface BuiltPluginPageBundle {
3
+ content: string;
4
+ hash: string;
5
+ size: number;
6
+ chunks: number;
7
+ }
8
+ export declare function buildPluginPageBundle(entry: string, pageEntry: string): BuiltPluginPageBundle;