@mearl/client 2.20.3 → 2.20.5

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.
@@ -0,0 +1,209 @@
1
+ import type { AppProfileBinding, AppProfilePolicies, AppProfileSetting } from './generated-app-profile-definition.js';
2
+ import type { BrowserDevicePresetName } from './generated-browser-action-protocol.js';
3
+ type DeviceCutout = 'none' | 'dynamic-island' | 'notch' | 'side-camera';
4
+ /** 桥调用风格,与页面适配器使用的端内调用约定一一对应。 */
5
+ export type AppBridgeStyle = 'windvane' | 'fliggy' | 'alipay' | 'callback' | 'btrip' | 'invoke' | 'callHandler' | 'amap' | 'didi' | 'easybikeNative' | 'mega' | 'wechat' | 'wechatMiniProgram';
6
+ export interface AppBridgeSpec {
7
+ global: string;
8
+ style: AppBridgeStyle;
9
+ /** 页面脚本重设全局对象时仍保留桥叶子,同时接纳页面对象上的其他 API。 */
10
+ persistent?: boolean;
11
+ }
12
+ export type AppBridgeBehavior = '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';
13
+ /** 桥方法名到标准化动作的映射。WindVane 风格使用 `Class.method`,其余使用 api 名。 */
14
+ export interface BridgeActionMap {
15
+ open: string[];
16
+ close: string[];
17
+ title: string[];
18
+ navigationBar?: string[];
19
+ statusBar?: string[];
20
+ systemInfo: string[];
21
+ mtop: string[];
22
+ kvGet?: string[];
23
+ kvSet?: string[];
24
+ kvDelete?: string[];
25
+ pageContext?: string[];
26
+ responses?: Record<string, unknown>;
27
+ behaviors?: Record<string, AppBridgeBehavior>;
28
+ noop: string[];
29
+ silent?: string[];
30
+ }
31
+ export interface AppSchemeRule {
32
+ match: string;
33
+ /** 规则仅在目标或当前页面 host 命中这些根域时生效。 */
34
+ hostSuffixes?: string[];
35
+ param?: string;
36
+ /** 将参数解出的 H5 地址保留 path/query/hash,并投影到当前页面 origin。 */
37
+ useCurrentOrigin?: boolean;
38
+ targetPath?: string;
39
+ targetHostPrefix?: string;
40
+ targetParams?: Record<string, string>;
41
+ }
42
+ export interface AppMiniProgramPaymentRoute {
43
+ /** 小程序支付中转页 pathname。 */
44
+ pathname: string;
45
+ /** 限制业务根域,避免同名小程序路由被其他项目接管。 */
46
+ hostSuffixes?: string[];
47
+ /** JSON 字符串形式的支付桥入参。 */
48
+ paymentParamsParam: string;
49
+ /** 按顺序读取的支付成功回跳参数,前者为空时使用后者。 */
50
+ successUrlParams: string[];
51
+ /** 按顺序读取的支付失败或取消回跳参数。 */
52
+ failureUrlParams: string[];
53
+ /** 支付成功结果写入回跳 URL 的参数名。 */
54
+ successResultParam?: string;
55
+ /** 从支付页路由透传到回跳 URL 的默认参数。 */
56
+ forwardParams?: string[];
57
+ /** 将回跳 H5 的 path/query/hash 投影到当前页面 origin。 */
58
+ useCurrentOrigin?: boolean;
59
+ }
60
+ export type AppMiniProgramMessageAction = 'stageBackData' | 'closeAll';
61
+ export interface AppMiniProgramMessageRule {
62
+ /** H5 通过 wx.miniProgram.postMessage 发送的 type。 */
63
+ type: string;
64
+ /** 容器只提供有限的页面栈动作,业务消息名由档案映射。 */
65
+ action: AppMiniProgramMessageAction;
66
+ /** 限制发送消息的 H5 根域。 */
67
+ hostSuffixes?: string[];
68
+ }
69
+ export type AppMiniProgramNativeAction = 'navigateMiniProgram' | 'subscribeMessage';
70
+ export interface AppMiniProgramNativeRoute {
71
+ /** 承接微信原生能力的小程序页面 pathname。 */
72
+ pathname: string;
73
+ /** 该页面允许模拟的固定原生动作,动作名读取 URL 的 action 参数。 */
74
+ actions: AppMiniProgramNativeAction[];
75
+ /** 限制业务根域,避免其他小程序的同名页面被接管。 */
76
+ hostSuffixes?: string[];
77
+ }
78
+ /** 微信小程序容器相关规则统一收口,避免为每种业务页增加顶层配置。 */
79
+ export interface AppMiniProgramConfig {
80
+ paymentRoutes?: AppMiniProgramPaymentRoute[];
81
+ nativeRoutes?: AppMiniProgramNativeRoute[];
82
+ messageRules?: AppMiniProgramMessageRule[];
83
+ }
84
+ export interface AppImagePickerConfig {
85
+ schemes: string[];
86
+ api: string;
87
+ version: string;
88
+ fcId: string;
89
+ fcGroup: string;
90
+ uploadUrlFunction: string;
91
+ prefix: string;
92
+ region: string;
93
+ }
94
+ export interface AppOverlayConfig {
95
+ enabledParam: string;
96
+ topRatioParam: string;
97
+ /** 弹层高度占屏比例参数,与 topRatioParam 二选一即可。 */
98
+ heightRatioParam?: string;
99
+ radiusParam: string;
100
+ maskAlphaParam: string;
101
+ tapCloseParam: string;
102
+ /** 蒙层关闭时优先触发当前 WebView 内的关闭控件,以复用页面定义的回传逻辑。 */
103
+ tapCloseSelectors?: string[];
104
+ /** 蒙层点击只退出当前 WebView,不调用页面关闭控件或打开回调。 */
105
+ silentTapClose?: boolean;
106
+ defaultTopRatio: number;
107
+ defaultRadius: number;
108
+ defaultMaskAlpha: number;
109
+ defaultTapClose: boolean;
110
+ }
111
+ export interface AppNativeBackConfig {
112
+ /** 原生返回键触发的 document 事件。 */
113
+ event?: string;
114
+ /** 返回 true 时由页面接管返回,再分发 event。 */
115
+ controlFunction?: string;
116
+ /** 原生返回键直接调用的 window 函数。 */
117
+ handler?: string;
118
+ }
119
+ export interface AppLayerUrlProxyConfig {
120
+ sourceHostPrefix: string;
121
+ pathPrefix: string;
122
+ targetHostSuffixes: string[];
123
+ defaultTargetHost?: string;
124
+ }
125
+ export interface AppTitleBarConfig {
126
+ navigationBarHeight: number;
127
+ backgroundColor: string;
128
+ textColor: string;
129
+ hiddenPathnames?: string[];
130
+ statusBarBackgroundColors?: Record<string, string>;
131
+ hostStyles?: AppTitleBarHostStyle[];
132
+ }
133
+ export interface AppTitleBarHostStyle {
134
+ /** 同时匹配该 host 与其全部子域,配置值不含前导点。 */
135
+ hostSuffix: string;
136
+ titleBarBackgroundColor?: string;
137
+ statusBarBackgroundColor?: string;
138
+ }
139
+ export interface AppSafeAreaInsets {
140
+ top: number;
141
+ right: number;
142
+ bottom: number;
143
+ left: number;
144
+ }
145
+ /** App 运行时使用的设备外观快照,避免通过机型字符串反推物理特征。 */
146
+ export interface AppDeviceAppearance {
147
+ preset: BrowserDevicePresetName;
148
+ orientation: 'portrait' | 'landscape';
149
+ cutout: DeviceCutout;
150
+ }
151
+ export interface AppRootContainerCookie {
152
+ name: string;
153
+ value: string;
154
+ domain?: string;
155
+ path?: string;
156
+ }
157
+ export interface AppRootContainerConfig {
158
+ enabled: boolean;
159
+ initialCookies?: AppRootContainerCookie[];
160
+ /** 横屏设备外壳中保留根页面,并将后续业务 WebView 呈现在右半屏。 */
161
+ splitScreen?: boolean;
162
+ }
163
+ /** 注入页内的运行时配置,可经 Server-Timing 或 CDP new-document 脚本传输。 */
164
+ export interface InjectedAppProfileConfig {
165
+ binding?: AppProfileBinding;
166
+ policies?: AppProfilePolicies;
167
+ settings?: AppProfileSetting[];
168
+ profile: string;
169
+ label: string;
170
+ globals: Record<string, string>;
171
+ bridges: AppBridgeSpec[];
172
+ activeEvents: string[];
173
+ maxTouchPoints: number;
174
+ statusBarHeight: number;
175
+ safeAreaInsets?: AppSafeAreaInsets;
176
+ /** 可选,以兼容未携带设备外观快照的持久化配置。 */
177
+ deviceAppearance?: AppDeviceAppearance;
178
+ userAgent: string;
179
+ navigatorPlatform: string;
180
+ platform: string;
181
+ systemVersion: string;
182
+ clientType: string;
183
+ language: string;
184
+ actions: BridgeActionMap;
185
+ schemes: AppSchemeRule[];
186
+ miniProgram?: AppMiniProgramConfig;
187
+ imagePicker?: AppImagePickerConfig;
188
+ /** 原生定位返回地址时使用的逆地理编码服务,复用容器的 MTop 传输。 */
189
+ geolocation?: {
190
+ reverseGeocodeApi: string;
191
+ version: string;
192
+ };
193
+ overlay?: AppOverlayConfig;
194
+ /** URL 参数为真时以新 WebView 替换当前业务层。 */
195
+ popBeforeOpenParam?: string;
196
+ /** 跨 WebView 消息抵达 opener 时分发的 document 事件。 */
197
+ messageEvent?: string;
198
+ nativeBack?: AppNativeBackConfig;
199
+ layerUrlProxy?: AppLayerUrlProxyConfig;
200
+ urlSyncHostSuffixes?: string[];
201
+ animationMs?: number;
202
+ forceH5Mtop?: boolean;
203
+ titleBar?: AppTitleBarConfig;
204
+ rootContainer?: AppRootContainerConfig;
205
+ simulateSystemBars?: boolean;
206
+ /** 物理状态栏与底部操作区由独立设备运行时渲染;本运行时只保留客户端导航栏。 */
207
+ externalDeviceChrome?: boolean;
208
+ }
209
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -512,17 +512,22 @@ export type TabGroupParams = {
512
512
  * 因此新增档案时只需改两处(名单 + 档案数据),且漏改会在类型检查时暴露。
513
513
  */
514
514
  export declare const APP_PROFILE_NAMES: readonly ["fliggy", "taobao", "wechat", "tmall", "alipay", "koubei", "taobao-tj", "taobao-live", "xianyu", "etao", "eleme", "damai", "btrip", "amap", "qianwen", "lingyou", "didi", "easybike"];
515
- export type AppProfileName = (typeof APP_PROFILE_NAMES)[number];
515
+ export type BuiltinAppProfileName = (typeof APP_PROFILE_NAMES)[number];
516
+ export type AppProfileName = BuiltinAppProfileName | `${string}/${string}`;
516
517
  export interface SetAppProfileParams {
517
518
  tabId: number;
518
519
  /** false 时清除该 tab 的容器模拟并刷新 */
519
520
  enabled: boolean;
520
521
  /** 容器档案名(enabled=true 时必填) */
521
522
  profile?: AppProfileName;
523
+ /** Immediately use the latest installed definition while preserving explicit options. */
524
+ refreshDefinition?: boolean;
525
+ /** 固定当前档案定义;默认在下次文档导航时自动采用可用更新。 */
526
+ lockDefinition?: boolean;
522
527
  os?: 'ios' | 'android';
523
528
  appVersion?: string;
524
- /** 支付宝容器语言;其他容器忽略。 */
525
- language?: 'zh-CN' | 'zh-TW' | 'zh-HK' | 'en-US';
529
+ /** 端定义支持的语言选项。 */
530
+ language?: string;
526
531
  deviceModel?: string;
527
532
  /** 按方法覆盖档案内置桥配置,仅支持受控行为、固定 JSON 回包和成功空实现 */
528
533
  bridgeConfig?: AppBridgeConfig;
@@ -0,0 +1,21 @@
1
+ export type NativeHostIntegrationMode = 'skills' | 'mcp';
2
+ export declare const MEARL_NATIVE_HOST_NAMES: Record<NativeHostIntegrationMode, string>;
3
+ export interface NativeHostDirectoryOptions {
4
+ env?: NodeJS.ProcessEnv;
5
+ homedir?: () => string;
6
+ platform?: NodeJS.Platform;
7
+ }
8
+ export declare function resolveNativeHostDirectory(options?: NativeHostDirectoryOptions): string;
9
+ export interface NativeHostDiagnostic {
10
+ label: string;
11
+ details: string[];
12
+ }
13
+ export interface NativeHostLauncherMetadata {
14
+ version: 1;
15
+ nodePaths: string[];
16
+ hostPath: string;
17
+ }
18
+ /** Inspect registrations even when Chrome cannot start the native host. Never execute launchers. */
19
+ export declare function inspectNativeHostInstallation(options?: NativeHostDirectoryOptions & {
20
+ hostDir?: string;
21
+ }): NativeHostDiagnostic[];
@@ -0,0 +1,107 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ export const MEARL_NATIVE_HOST_NAMES = {
5
+ skills: 'com.alibaba.mearl.skills',
6
+ mcp: 'com.alibaba.mearl.mcp',
7
+ };
8
+ export function resolveNativeHostDirectory(options = {}) {
9
+ const env = options.env ?? process.env;
10
+ const home = (options.homedir ?? os.homedir)();
11
+ switch (options.platform ?? process.platform) {
12
+ case 'darwin':
13
+ return path.join(home, 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
14
+ case 'linux':
15
+ return path.join(home, '.config/google-chrome/NativeMessagingHosts');
16
+ case 'win32':
17
+ return path.join(env.PROGRAMDATA || 'C:\\ProgramData', 'Google\\Chrome\\NativeMessagingHosts');
18
+ default:
19
+ throw new Error(`Unsupported platform: ${String(options.platform ?? process.platform)}`);
20
+ }
21
+ }
22
+ function readLauncherMetadata(content) {
23
+ const metadata = content.match(/^(?:#|rem) mearl-native-host-launcher: (.+)\r?$/m);
24
+ if (metadata) {
25
+ const parsed = JSON.parse(metadata[1]);
26
+ if (parsed.version === 1 &&
27
+ Array.isArray(parsed.nodePaths) &&
28
+ parsed.nodePaths.length > 0 &&
29
+ parsed.nodePaths.every(value => typeof value === 'string' && path.isAbsolute(value)) &&
30
+ typeof parsed.hostPath === 'string' &&
31
+ path.isAbsolute(parsed.hostPath)) {
32
+ return { version: 1, nodePaths: parsed.nodePaths, hostPath: parsed.hostPath };
33
+ }
34
+ throw new Error('Invalid native host launcher metadata');
35
+ }
36
+ // Legacy launchers stored these assignments as JSON strings.
37
+ const node = content.match(/^NODE_BIN=(".*")\r?$/m);
38
+ const host = content.match(/^HOST_SCRIPT=(".*")\r?$/m);
39
+ if (!node || !host)
40
+ return null;
41
+ const nodePath = JSON.parse(node[1]);
42
+ const hostPath = JSON.parse(host[1]);
43
+ if (typeof nodePath !== 'string' ||
44
+ !path.isAbsolute(nodePath) ||
45
+ typeof hostPath !== 'string' ||
46
+ !path.isAbsolute(hostPath)) {
47
+ throw new Error('Invalid native host launcher paths');
48
+ }
49
+ return { version: 1, nodePaths: [nodePath], hostPath };
50
+ }
51
+ /** Inspect registrations even when Chrome cannot start the native host. Never execute launchers. */
52
+ export function inspectNativeHostInstallation(options = {}) {
53
+ const platform = options.platform ?? process.platform;
54
+ if (!['darwin', 'linux', 'win32'].includes(platform))
55
+ return [];
56
+ const hostDir = options.hostDir ?? resolveNativeHostDirectory(options);
57
+ const diagnostics = [];
58
+ const isUsable = (filePath, executable) => {
59
+ try {
60
+ fs.accessSync(filePath, executable && platform !== 'win32' ? fs.constants.X_OK : fs.constants.R_OK);
61
+ return fs.statSync(filePath).isFile();
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ };
67
+ for (const hostName of Object.values(MEARL_NATIVE_HOST_NAMES)) {
68
+ const manifestPath = path.join(hostDir, `${hostName}.json`);
69
+ const details = [];
70
+ try {
71
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
72
+ if (typeof manifest?.path !== 'string' || !path.isAbsolute(manifest.path)) {
73
+ details.push(`Invalid launcher path in ${manifestPath}`);
74
+ }
75
+ else if (!isUsable(manifest.path, true)) {
76
+ details.push(`Launcher is missing or not executable: ${manifest.path}`);
77
+ }
78
+ else {
79
+ const metadata = readLauncherMetadata(fs.readFileSync(manifest.path, 'utf-8'));
80
+ if (metadata) {
81
+ if (!metadata.nodePaths.some(candidate => isUsable(candidate, true))) {
82
+ details.push(`No executable Node.js found. Launcher candidates: ${metadata.nodePaths.join(', ')}`);
83
+ }
84
+ if (!isUsable(metadata.hostPath, false)) {
85
+ details.push(`Native host runtime script is missing or unreadable: ${metadata.hostPath}`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ catch (error) {
91
+ if (error.code === 'ENOENT' && !fs.existsSync(manifestPath))
92
+ continue;
93
+ details.push(`Cannot inspect ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`);
94
+ }
95
+ if (details.length) {
96
+ diagnostics.push({
97
+ label: `Native Messaging registration needs repair: ${hostName}`,
98
+ details: [
99
+ ...details,
100
+ `Registration: ${manifestPath}`,
101
+ 'Run `mearl-native-host init` with a working Node.js, then reopen the extension. If the runtime package is missing, run `npx @mearl/setup update --local`.',
102
+ ],
103
+ });
104
+ }
105
+ }
106
+ return diagnostics;
107
+ }
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { AppProfileContribution } from './generated-app-profile-definition.js';
2
+ export type { AppProfileContribution, AppProfileDefinition, } from './generated-app-profile-definition.js';
1
3
  export { invokePageEvalWithArgs } from './pageEvalArguments.js';
2
4
  import { type OfficialPluginInstaller } from './officialPluginInstaller.js';
3
5
  import { type PluginToolboxContribution, type PluginRegistration } from './pluginRegistry.js';
@@ -38,6 +40,8 @@ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invo
38
40
  name: string | undefined;
39
41
  description: string | undefined;
40
42
  toolbox: PluginToolboxContribution | undefined;
43
+ appProfiles: AppProfileContribution | undefined;
44
+ profileSources: Record<string, string> | undefined;
41
45
  version: string | undefined;
42
46
  commandHelp(namespace: string): string;
43
47
  help(namespace: string, name?: string): string;
@@ -48,6 +52,8 @@ export declare function loadRegisteredPlugin(name: string, context: PluginExecut
48
52
  name: string | undefined;
49
53
  description: string | undefined;
50
54
  toolbox: PluginToolboxContribution | undefined;
55
+ appProfiles: AppProfileContribution | undefined;
56
+ profileSources: Record<string, string> | undefined;
51
57
  version: string | undefined;
52
58
  commandHelp(namespace: string): string;
53
59
  help(namespace: string, name?: string): string;
package/dist/plugin.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { loadPluginAppProfiles } from './pluginAppProfiles.js';
1
2
  export { invokePageEvalWithArgs } from './pageEvalArguments.js';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import { dirname, extname, resolve } from 'node:path';
@@ -107,6 +108,8 @@ export async function refreshRegisteredPlugin(name, includePage = false) {
107
108
  }
108
109
  registerPlugin(name, registration.entry, loaded.description, {
109
110
  toolbox: loaded.toolbox,
111
+ appProfiles: loaded.appProfiles,
112
+ profileSources: 'profileSources' in loaded ? loaded.profileSources : loaded.source?.profiles,
110
113
  ...(registration.managedBy ? { managedBy: registration.managedBy } : {}),
111
114
  });
112
115
  return findPluginRegistration(name);
@@ -134,6 +137,8 @@ export async function loadPlugin(entry, context, pluginName) {
134
137
  let name;
135
138
  let description;
136
139
  let toolbox;
140
+ let appProfiles;
141
+ let profileSources;
137
142
  const registerCommand = (signature, handler, description = '', examples = [], toolboxOnly = false) => {
138
143
  if (!registering)
139
144
  throw new Error('命令只能在入口注册阶段声明');
@@ -178,6 +183,7 @@ export async function loadPlugin(entry, context, pluginName) {
178
183
  name = descriptor.name;
179
184
  description = descriptor.description;
180
185
  toolbox = normalizePluginToolboxContribution(descriptor.toolbox);
186
+ ({ appProfiles, profileSources } = loadPluginAppProfiles(entry, descriptor.appProfiles));
181
187
  for (const command of descriptor.commands) {
182
188
  const commandName = command.signature.trim().split(/\s+/)[0];
183
189
  const register = command.toolboxOnly ? app.toolboxCommand : app.command;
@@ -195,11 +201,15 @@ export async function loadPlugin(entry, context, pluginName) {
195
201
  throw new Error('入口的 description 导出必须为字符串');
196
202
  description = module.description;
197
203
  toolbox = normalizePluginToolboxContribution(module.toolbox);
198
- if (typeof module.default !== 'function')
204
+ ({ appProfiles, profileSources } = loadPluginAppProfiles(entry, module.appProfiles));
205
+ if (module.default !== undefined && typeof module.default !== 'function')
206
+ throw new Error('入口必须默认导出 register(app) 函数');
207
+ if (typeof module.default === 'function')
208
+ await module.default(app);
209
+ else if (!appProfiles)
199
210
  throw new Error('入口必须默认导出 register(app) 函数');
200
- await module.default(app);
201
211
  }
202
- if (commands.size === 0)
212
+ if (commands.size === 0 && !appProfiles)
203
213
  throw new Error('入口未注册任何命令');
204
214
  const toolboxCommands = toolbox?.page.commands ?? [];
205
215
  for (const [name, command] of commands) {
@@ -290,6 +300,8 @@ export async function loadPlugin(entry, context, pluginName) {
290
300
  name,
291
301
  description,
292
302
  toolbox,
303
+ appProfiles,
304
+ profileSources,
293
305
  version: readPluginVersion(entry),
294
306
  commandHelp(namespace) {
295
307
  return publicCommands()
@@ -0,0 +1,6 @@
1
+ import { type AppProfileContribution } from './generated-app-profile-definition.js';
2
+ /** Profile resources share the Skill root and metadata refresh rules of toolbox resources. */
3
+ export declare function loadPluginAppProfiles(entry: string, value: unknown): {
4
+ appProfiles?: AppProfileContribution;
5
+ profileSources?: Record<string, string>;
6
+ };
@@ -0,0 +1,40 @@
1
+ import { readFileSync, realpathSync } from 'node:fs';
2
+ import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
3
+ import { validateAppProfileContribution, } from './generated-app-profile-definition.js';
4
+ import { pluginFileVersion } from './pluginSource.js';
5
+ /** Profile resources share the Skill root and metadata refresh rules of toolbox resources. */
6
+ export function loadPluginAppProfiles(entry, value) {
7
+ if (value === undefined)
8
+ return {};
9
+ if (!value || typeof value !== 'object' || Array.isArray(value))
10
+ throw new Error('appProfiles must be an object');
11
+ const declaration = value;
12
+ if (!Array.isArray(declaration.profiles))
13
+ throw new Error('appProfiles.profiles must be an array');
14
+ const root = realpathSync(basename(dirname(entry)) === 'scripts' ? dirname(dirname(entry)) : dirname(entry));
15
+ const sources = {};
16
+ const profiles = declaration.profiles.map(profile => {
17
+ if (typeof profile !== 'string')
18
+ return profile;
19
+ if (isAbsolute(profile) || !profile.endsWith('.json'))
20
+ throw new Error('Profile resource must be a relative JSON path');
21
+ const reference = resolve(root, profile);
22
+ const file = realpathSync(reference);
23
+ const inside = relative(root, file);
24
+ if (inside.startsWith('..') || isAbsolute(inside))
25
+ throw new Error('Profile resource must stay inside the Skill directory');
26
+ const version = pluginFileVersion(file);
27
+ const content = readFileSync(file, 'utf8');
28
+ if (content.length > 65_536)
29
+ throw new Error('Profile resource exceeds 64 KiB');
30
+ const parsed = JSON.parse(content);
31
+ if (pluginFileVersion(reference) !== version)
32
+ throw new Error('Profile resource changed during registration');
33
+ sources[reference] = version;
34
+ return parsed;
35
+ });
36
+ return {
37
+ appProfiles: validateAppProfileContribution({ ...declaration, profiles }),
38
+ profileSources: sources,
39
+ };
40
+ }
@@ -30,6 +30,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
30
30
  let requestSequence = 0;
31
31
  const pending = new Map();
32
32
  const pageEventListeners = new Map();
33
+ const contextListeners = new Set();
33
34
  let subscriptionSequence = 0;
34
35
  let resolveReady;
35
36
  const ready = new Promise(resolve => { resolveReady = resolve; });
@@ -397,7 +398,13 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
397
398
  return query;
398
399
  };
399
400
 
400
- const api = { invoke, subscribe, ready: () => ready, runAgentTask, setAgentGuides, setAgentPanelVisible, setQueryParam };
401
+ const onContextChange = listener => {
402
+ if (typeof listener !== 'function') throw new Error('Invalid context listener');
403
+ contextListeners.add(listener);
404
+ if (context) listener(context);
405
+ return () => contextListeners.delete(listener);
406
+ };
407
+ const api = { invoke, subscribe, onContextChange, ready: async () => { await ready; return context; }, runAgentTask, setAgentGuides, setAgentPanelVisible, setQueryParam };
401
408
  const ui = globalThis.__mearlToolboxUi;
402
409
  if (ui !== undefined) {
403
410
  if (!ui || typeof ui !== 'object' || ui.runtime !== 'preact') {
@@ -421,6 +428,10 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
421
428
  document.documentElement.style.colorScheme = theme;
422
429
  if (/^\d+(?:\.\d+)?px$/.test(fontSize)) document.documentElement.style.fontSize = fontSize;
423
430
  context = Object.freeze({ ...(context || {}), ...(nextContext || {}), theme });
431
+ for (const listener of contextListeners) {
432
+ try { listener(context); }
433
+ catch (error) { console.error('Toolbox context listener failed', error); }
434
+ }
424
435
  return context;
425
436
  };
426
437
 
@@ -1,3 +1,4 @@
1
+ import { type AppProfileContribution } from './generated-app-profile-definition.js';
1
2
  export declare class PluginError extends Error {
2
3
  readonly code: string;
3
4
  constructor(code: string, message: string);
@@ -7,6 +8,9 @@ export interface PluginToolboxSandboxPage {
7
8
  runtime?: 'preact';
8
9
  entry: string;
9
10
  commands: string[];
11
+ lifecycle?: {
12
+ onNavigation: string;
13
+ };
10
14
  }
11
15
  export interface PluginToolboxLocale {
12
16
  title?: string;
@@ -37,9 +41,11 @@ export interface PluginRegistration {
37
41
  description?: string;
38
42
  managedBy?: string;
39
43
  toolbox?: PluginToolboxContribution;
44
+ appProfiles?: AppProfileContribution;
40
45
  pageBundle?: PluginPageBundleRegistration;
41
46
  source?: {
42
47
  entry: string;
48
+ profiles?: Record<string, string>;
43
49
  page?: Record<string, string>;
44
50
  pageRuntime?: string;
45
51
  };
@@ -48,6 +54,8 @@ export declare function pluginRegistrationNeedsRefresh(registration: PluginRegis
48
54
  export interface PluginRegistrationOptions {
49
55
  managedBy?: string;
50
56
  toolbox?: PluginToolboxContribution;
57
+ appProfiles?: AppProfileContribution;
58
+ profileSources?: Record<string, string>;
51
59
  }
52
60
  export declare function pluginDirectory(): string;
53
61
  export declare function assertPluginName(name: string): void;
@@ -1,3 +1,4 @@
1
+ import { validateAppProfileContribution, } from './generated-app-profile-definition.js';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
4
  import { homedir } from 'node:os';
@@ -25,6 +26,15 @@ export function pluginRegistrationNeedsRefresh(registration, includePage = false
25
26
  return true;
26
27
  if (pluginFileVersion(registration.entry) !== registration.source.entry)
27
28
  return true;
29
+ if (Object.entries(registration.source.profiles ?? {}).some(([path, version]) => {
30
+ try {
31
+ return pluginFileVersion(path) !== version;
32
+ }
33
+ catch {
34
+ return true;
35
+ }
36
+ }))
37
+ return true;
28
38
  if (!includePage || !registration.toolbox)
29
39
  return false;
30
40
  if (registration.source.pageRuntime !== PLUGIN_PAGE_RUNTIME_HASH)
@@ -199,6 +209,17 @@ export function normalizePluginToolboxContribution(value) {
199
209
  entry: pageEntry,
200
210
  commands,
201
211
  };
212
+ if (pageCandidate.lifecycle !== undefined) {
213
+ const lifecycle = pageCandidate.lifecycle;
214
+ if (!lifecycle ||
215
+ typeof lifecycle !== 'object' ||
216
+ Array.isArray(lifecycle) ||
217
+ typeof lifecycle.onNavigation !== 'string' ||
218
+ !commands.includes(lifecycle.onNavigation)) {
219
+ throw new Error('toolbox.page.lifecycle.onNavigation 必须是 page.commands 中的命令名称');
220
+ }
221
+ page.lifecycle = { onNavigation: lifecycle.onNavigation };
222
+ }
202
223
  let agent;
203
224
  if (candidate.agent !== undefined) {
204
225
  if (!candidate.agent || typeof candidate.agent !== 'object' || Array.isArray(candidate.agent)) {
@@ -290,11 +311,15 @@ export function registerPlugin(name, target, description, options = {}) {
290
311
  entry,
291
312
  source: {
292
313
  entry: entryVersion,
314
+ ...(options.profileSources ? { profiles: options.profileSources } : {}),
293
315
  ...(builtPage ? { page: builtPage.sources, pageRuntime: PLUGIN_PAGE_RUNTIME_HASH } : {}),
294
316
  },
295
317
  ...(summary ? { description: summary } : {}),
296
318
  ...(managedBy ? { managedBy } : {}),
297
319
  ...(toolbox ? { toolbox } : {}),
320
+ ...(options.appProfiles
321
+ ? { appProfiles: validateAppProfileContribution(options.appProfiles) }
322
+ : {}),
298
323
  ...(builtPage
299
324
  ? { pageBundle: { hash: builtPage.hash, size: builtPage.size, chunks: builtPage.chunks } }
300
325
  : {}),
@@ -348,12 +373,18 @@ export function findPluginRegistration(name) {
348
373
  : undefined;
349
374
  const toolbox = normalizePluginToolboxContribution(record.toolbox);
350
375
  const pageBundle = toolbox ? normalizePageBundle(record.pageBundle) : undefined;
376
+ const appProfiles = validateAppProfileContribution(record.appProfiles);
351
377
  const source = record.source;
352
378
  if (source !== undefined &&
353
379
  (!source ||
354
380
  typeof source !== 'object' ||
355
381
  Array.isArray(source) ||
356
382
  typeof source.entry !== 'string' ||
383
+ (source.profiles !== undefined &&
384
+ (!source.profiles ||
385
+ typeof source.profiles !== 'object' ||
386
+ Array.isArray(source.profiles) ||
387
+ Object.entries(source.profiles).some(([path, version]) => !isAbsolute(path) || typeof version !== 'string'))) ||
357
388
  (source.pageRuntime !== undefined &&
358
389
  (typeof source.pageRuntime !== 'string' || !/^[a-f0-9]{64}$/.test(source.pageRuntime))) ||
359
390
  (source.page !== undefined &&
@@ -369,6 +400,7 @@ export function findPluginRegistration(name) {
369
400
  ...(managedBy ? { managedBy } : {}),
370
401
  ...(toolbox ? { toolbox } : {}),
371
402
  ...(pageBundle ? { pageBundle } : {}),
403
+ ...(appProfiles ? { appProfiles } : {}),
372
404
  ...(source ? { source } : {}),
373
405
  };
374
406
  }
@@ -9,6 +9,7 @@ export interface PythonPluginDescriptor {
9
9
  description?: string;
10
10
  name?: string;
11
11
  toolbox?: unknown;
12
+ appProfiles?: unknown;
12
13
  }
13
14
  export interface PythonPluginApp {
14
15
  readonly options: Readonly<PluginOptions>;