@mearl/browser-core 3.0.0-beta.9 → 3.0.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/README.en.md +13 -0
- package/README.md +3 -1
- package/dist/agentChat.d.ts +8 -1
- package/dist/agentChat.js +8 -0
- package/dist/agentServiceProtocol.d.ts +23 -0
- package/dist/agentServiceProtocol.js +3 -0
- package/dist/appProfileDefinition.d.ts +3 -1
- package/dist/appProfileDefinition.js +5 -0
- package/dist/browserActionProtocol.d.ts +11 -11
- package/dist/deviceEmulation.d.ts +6 -0
- package/dist/deviceEmulation.js +13 -0
- package/dist/extensionServiceProtocol.d.ts +7 -0
- package/dist/extensionServiceProtocol.js +8 -0
- package/dist/pluginPageHost.js +4 -3
- package/dist/pluginPages.d.ts +1 -0
- package/dist/pluginPages.js +10 -1
- package/dist/proxyPermissions.js +1 -0
- package/dist/rootWebViewDocument.js +35 -0
- package/dist/rootWebViewNavigation.d.ts +11 -4
- package/dist/rootWebViewNavigation.js +75 -18
- package/dist/toolbox.d.ts +1 -0
- package/package.json +2 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @mearl/browser-core
|
|
2
|
+
|
|
3
|
+
[简体中文](README.md) | [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
Page control logic and protocols shared by browsers and Node: DOM/AX snapshots, action observation, input, scrolling, navigation, request serialization, capture cursors, device simulation, generic device appearance, Cookie/domain permissions, and plugin task, page, and state contracts. Modules have no browser extension or native process installation side effects.
|
|
6
|
+
|
|
7
|
+
The core includes no preset App identities. The host validates profile IDs against its active capability catalog. App profile schemas, explicit options, revisions, and page runtime bindings are data contracts; independent capability packages supply complete bridge implementations.
|
|
8
|
+
|
|
9
|
+
The built-in installer at `@mearl/browser-core/appProfileRuntime` synchronizes only UA, navigator.appVersion, navigator.platform, and touch points. It does not install App bridges, business globals, container stacks, or authorization handlers. With a pageRuntime binding, the host must prepare the exact code from the installed capability package and execute it through the page runtime dispatcher; the base installer cannot substitute for this. See [browser bundles](../../docs/browser-bundles.en.md#page-runtimes) for resource preparation and dispatch.
|
|
10
|
+
|
|
11
|
+
The default proxy authorization addresses are localhost and 127.0.0.1. Hosts obtain permission for other sites and pass them in the authorization list. The core includes Aplus/RUM/ARMS event parsers; additional parsers register through capability packages.
|
|
12
|
+
|
|
13
|
+
Agent contracts are defined in agentChat.ts, agentSkills.ts, and pluginModels.ts and exported through the corresponding subpaths. agent-runtime uses these contracts to avoid duplicate definitions. Serializable page functions remain self-contained for shared use by the Extension and CDP.
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# @mearl/browser-core
|
|
2
2
|
|
|
3
|
+
[简体中文](README.md) | [English](README.en.md)
|
|
4
|
+
|
|
3
5
|
浏览器与 Node 可共享的页面控制逻辑和协议:DOM/AX 快照、动作观察、输入、滚动、导航、请求序列化、捕获游标、设备模拟、通用设备外观、Cookie/域名权限和插件任务、页面、状态合同。模块没有浏览器扩展或本机进程的安装副作用。
|
|
4
6
|
|
|
5
|
-
核心不预置 App
|
|
7
|
+
核心不预置 App 身份;端模拟 ID 由宿主的已激活能力目录校验。端模拟配置 schema、显式选项、revision 和页面运行时绑定是数据合同,完整桥实现由独立能力包提供。
|
|
6
8
|
|
|
7
9
|
`@mearl/browser-core/appProfileRuntime` 的内置安装器只同步 UA、navigator.appVersion、navigator.platform 和触摸点,不安装 App 桥、业务全局变量、容器栈或授权处理。有 pageRuntime 绑定时,宿主必须先从已安装的能力包准备确切代码,再通过页面运行时派发器执行;不能调用基础安装器代替。资源准备和派发见 [能力包](../../docs/browser-bundles.md#页面运行时)。
|
|
8
10
|
|
package/dist/agentChat.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type AgentChatEventKind = 'lifecycle' | 'notification' | 'request' | 'error';
|
|
2
2
|
export type AgentChatSessionStatus = 'idle' | 'running' | 'waiting' | 'error';
|
|
3
|
+
export declare function isQoderAuthenticationError(message: unknown): boolean;
|
|
4
|
+
export declare function isQoderMissingSessionError(message: unknown): boolean;
|
|
3
5
|
export type AgentChatCapabilitySupport = boolean | 'runtime';
|
|
4
6
|
export declare const AGENT_CHAT_MAX_PAGE_URL_CHARS = 16384;
|
|
5
7
|
export declare const AGENT_CHAT_MAX_PAGE_TITLE_CHARS = 2048;
|
|
@@ -28,7 +30,8 @@ export interface AgentChatProviderDescriptor {
|
|
|
28
30
|
};
|
|
29
31
|
error?: string;
|
|
30
32
|
setup?: {
|
|
31
|
-
installCommand
|
|
33
|
+
installCommand?: string;
|
|
34
|
+
canInstall?: boolean;
|
|
32
35
|
loginCommand?: string;
|
|
33
36
|
docsUrl?: string;
|
|
34
37
|
};
|
|
@@ -91,6 +94,7 @@ export interface AgentChatSendOptions {
|
|
|
91
94
|
message: string;
|
|
92
95
|
images?: AgentChatImageInput[];
|
|
93
96
|
displayMessage?: string;
|
|
97
|
+
hasAdditionalContext?: boolean;
|
|
94
98
|
clientMessageId?: string;
|
|
95
99
|
}
|
|
96
100
|
export interface AgentChatRunOptions {
|
|
@@ -157,6 +161,9 @@ export interface AgentChatStatus {
|
|
|
157
161
|
sessions: AgentChatSession[];
|
|
158
162
|
}
|
|
159
163
|
export interface AgentChatKnownEventParams {
|
|
164
|
+
'session/reconnected': {
|
|
165
|
+
providerSessionId: string;
|
|
166
|
+
};
|
|
160
167
|
'turn/started': {
|
|
161
168
|
turn: {
|
|
162
169
|
id: string;
|
package/dist/agentChat.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export function isQoderAuthenticationError(message) {
|
|
2
|
+
return (typeof message === 'string' &&
|
|
3
|
+
/^(?:Authentication (?:is )?required|Unauthenticated|Sign-in required)(?:[:.\s]|$)/i.test(message));
|
|
4
|
+
}
|
|
5
|
+
export function isQoderMissingSessionError(message) {
|
|
6
|
+
return typeof message === 'string' && /^(?:Invalid params:\s*)?Invalid session identifier\b/i.test(message);
|
|
7
|
+
}
|
|
1
8
|
export const AGENT_CHAT_MAX_PAGE_URL_CHARS = 16_384;
|
|
2
9
|
export const AGENT_CHAT_MAX_PAGE_TITLE_CHARS = 2_048;
|
|
3
10
|
export const AGENT_CHAT_MAX_TOOLBOX_CONTEXT_CHARS = 65_536;
|
|
@@ -123,6 +130,7 @@ function readAgentChatRequestParams(params) {
|
|
|
123
130
|
return { ...params, requestId: params.requestId };
|
|
124
131
|
}
|
|
125
132
|
const AGENT_CHAT_EVENT_PARAM_READERS = {
|
|
133
|
+
'session/reconnected': params => typeof params.providerSessionId === 'string' ? { providerSessionId: params.providerSessionId } : null,
|
|
126
134
|
'turn/started': readAgentChatTurnParams,
|
|
127
135
|
'turn/completed': readAgentChatTurnParams,
|
|
128
136
|
'session/model/updated': readAgentChatModelParams,
|
|
@@ -11,6 +11,18 @@ export interface AgentServiceActionMap {
|
|
|
11
11
|
request: undefined;
|
|
12
12
|
response: AgentChatStatus;
|
|
13
13
|
};
|
|
14
|
+
agent_chat_install_provider: {
|
|
15
|
+
request: {
|
|
16
|
+
providerId: string;
|
|
17
|
+
};
|
|
18
|
+
response: AgentChatStatus;
|
|
19
|
+
};
|
|
20
|
+
agent_chat_login_qoder: {
|
|
21
|
+
request: undefined;
|
|
22
|
+
response: {
|
|
23
|
+
url: string | null;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
14
26
|
agent_chat_prepare: {
|
|
15
27
|
request: AgentChatPrepareOptions;
|
|
16
28
|
response: {
|
|
@@ -51,6 +63,14 @@ export interface AgentServiceActionMap {
|
|
|
51
63
|
request: AgentChatEventsOptions;
|
|
52
64
|
response: AgentChatEventsResult;
|
|
53
65
|
};
|
|
66
|
+
agent_chat_read_visualization: {
|
|
67
|
+
request: {
|
|
68
|
+
path: string;
|
|
69
|
+
};
|
|
70
|
+
response: {
|
|
71
|
+
html: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
54
74
|
agent_chat_refresh: {
|
|
55
75
|
request: AgentChatEventsOptions;
|
|
56
76
|
response: AgentChatRefreshResult;
|
|
@@ -112,6 +132,8 @@ export type AgentServiceHandlerMap = {
|
|
|
112
132
|
export declare const AGENT_SERVICE_ACTIONS: {
|
|
113
133
|
readonly agent_chat_pick_directory: true;
|
|
114
134
|
readonly agent_chat_status: true;
|
|
135
|
+
readonly agent_chat_install_provider: true;
|
|
136
|
+
readonly agent_chat_login_qoder: true;
|
|
115
137
|
readonly agent_chat_prepare: true;
|
|
116
138
|
readonly agent_chat_list_sessions: true;
|
|
117
139
|
readonly agent_chat_start: true;
|
|
@@ -120,6 +142,7 @@ export declare const AGENT_SERVICE_ACTIONS: {
|
|
|
120
142
|
readonly agent_chat_interrupt: true;
|
|
121
143
|
readonly agent_chat_close: true;
|
|
122
144
|
readonly agent_chat_events: true;
|
|
145
|
+
readonly agent_chat_read_visualization: true;
|
|
123
146
|
readonly agent_chat_refresh: true;
|
|
124
147
|
readonly agent_chat_open_app: true;
|
|
125
148
|
readonly agent_chat_open_workspace: true;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const AGENT_SERVICE_ACTIONS = {
|
|
2
2
|
agent_chat_pick_directory: true,
|
|
3
3
|
agent_chat_status: true,
|
|
4
|
+
agent_chat_install_provider: true,
|
|
5
|
+
agent_chat_login_qoder: true,
|
|
4
6
|
agent_chat_prepare: true,
|
|
5
7
|
agent_chat_list_sessions: true,
|
|
6
8
|
agent_chat_start: true,
|
|
@@ -9,6 +11,7 @@ export const AGENT_SERVICE_ACTIONS = {
|
|
|
9
11
|
agent_chat_interrupt: true,
|
|
10
12
|
agent_chat_close: true,
|
|
11
13
|
agent_chat_events: true,
|
|
14
|
+
agent_chat_read_visualization: true,
|
|
12
15
|
agent_chat_refresh: true,
|
|
13
16
|
agent_chat_open_app: true,
|
|
14
17
|
agent_chat_open_workspace: true,
|
|
@@ -66,9 +66,11 @@ export interface AppProfileUsage {
|
|
|
66
66
|
url: string;
|
|
67
67
|
}>;
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
69
|
+
/** 单个 App 容器的声明式端模拟配置。 */
|
|
70
70
|
export interface AppProfileDefinition {
|
|
71
71
|
name: string;
|
|
72
|
+
/** Preferred App ID when the capability is installed outside a Preset. */
|
|
73
|
+
defaultAppId?: string;
|
|
72
74
|
usage?: AppProfileUsage;
|
|
73
75
|
/** Content-addressed installer supplied by the selected browser capability package. */
|
|
74
76
|
pageRuntime?: {
|
|
@@ -83,6 +83,7 @@ export function validateAppProfileDefinition(value) {
|
|
|
83
83
|
'schemes',
|
|
84
84
|
];
|
|
85
85
|
const optional = [
|
|
86
|
+
'defaultAppId',
|
|
86
87
|
'usage',
|
|
87
88
|
'actionIcon',
|
|
88
89
|
'labels',
|
|
@@ -114,6 +115,10 @@ export function validateAppProfileDefinition(value) {
|
|
|
114
115
|
invalid(key);
|
|
115
116
|
if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/.test(value.name))
|
|
116
117
|
invalid('name');
|
|
118
|
+
if (value.defaultAppId !== undefined &&
|
|
119
|
+
(typeof value.defaultAppId !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(value.defaultAppId) ||
|
|
120
|
+
['constructor', 'prototype'].includes(value.defaultAppId)))
|
|
121
|
+
invalid('defaultAppId');
|
|
117
122
|
if (value.usage !== undefined) {
|
|
118
123
|
const usage = value.usage;
|
|
119
124
|
fields(usage, [], ['description', 'limitations', 'skills', 'documentation'], 'usage');
|
|
@@ -464,9 +464,9 @@ export interface BrowserEmulationOptions {
|
|
|
464
464
|
userAgent?: string;
|
|
465
465
|
orientation?: 'portrait' | 'landscape';
|
|
466
466
|
touch?: boolean;
|
|
467
|
-
/** 在 iframe WebView
|
|
467
|
+
/** 在 iframe WebView 外渲染顶部状态栏和底部操作区;默认 false。 */
|
|
468
468
|
deviceShell?: boolean;
|
|
469
|
-
/** App 容器横屏时将根页面与业务页面分置左右 WebView
|
|
469
|
+
/** App 容器横屏时将根页面与业务页面分置左右 WebView,需要同时开启 deviceShell 和端模拟。 */
|
|
470
470
|
splitScreen?: boolean;
|
|
471
471
|
}
|
|
472
472
|
/** Provider-defined behavior ID; core does not execute or enumerate business behaviors. */
|
|
@@ -478,7 +478,7 @@ export interface AppBridgeMethodConfig {
|
|
|
478
478
|
noop?: boolean;
|
|
479
479
|
silent?: boolean;
|
|
480
480
|
}
|
|
481
|
-
/**
|
|
481
|
+
/** 追加或覆盖端模拟配置中的桥方法。配置保持 JSON 可序列化。 */
|
|
482
482
|
export interface AppBridgeConfig {
|
|
483
483
|
methods: Record<string, AppBridgeMethodConfig>;
|
|
484
484
|
}
|
|
@@ -499,13 +499,13 @@ export interface TabOpenParams {
|
|
|
499
499
|
emulation?: BrowserEmulationOptions;
|
|
500
500
|
group?: string;
|
|
501
501
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
502
|
+
* 端模拟配置 ID,通过 get_capabilities 查询。该配置提供容器 UA、环境变量、JSBridge 和桥协议。
|
|
503
|
+
* 设备视口由 emulation 配置,系统栏由 emulation.deviceShell 控制。
|
|
504
504
|
*/
|
|
505
505
|
appProfile?: AppProfileName;
|
|
506
506
|
/** 与 appProfile 配合使用;容器系统,默认 ios */
|
|
507
507
|
os?: 'ios' | 'android';
|
|
508
|
-
/** 与 appProfile
|
|
508
|
+
/** 与 appProfile 配合使用,按方法覆盖端模拟的内置桥配置。 */
|
|
509
509
|
bridgeConfig?: AppBridgeConfig;
|
|
510
510
|
}
|
|
511
511
|
export type TabGroupParams = {
|
|
@@ -526,18 +526,18 @@ export interface SetAppProfileParams {
|
|
|
526
526
|
tabId: number;
|
|
527
527
|
/** false 时清除该 tab 的容器模拟并刷新 */
|
|
528
528
|
enabled: boolean;
|
|
529
|
-
/**
|
|
529
|
+
/** 端模拟配置 ID(enabled=true 时必填)。 */
|
|
530
530
|
profile?: AppProfileName;
|
|
531
531
|
/** Immediately use the latest installed definition while preserving explicit options. */
|
|
532
532
|
refreshDefinition?: boolean;
|
|
533
|
-
/**
|
|
533
|
+
/** 固定当前端模拟配置。默认在下次文档导航时自动采用可用更新。 */
|
|
534
534
|
lockDefinition?: boolean;
|
|
535
535
|
os?: 'ios' | 'android';
|
|
536
536
|
appVersion?: string;
|
|
537
537
|
/** 端定义支持的语言选项。 */
|
|
538
538
|
language?: string;
|
|
539
539
|
deviceModel?: string;
|
|
540
|
-
/**
|
|
540
|
+
/** 按方法覆盖端模拟的内置桥配置,仅支持受控行为、固定 JSON 回包和成功空实现。 */
|
|
541
541
|
bridgeConfig?: AppBridgeConfig;
|
|
542
542
|
/**
|
|
543
543
|
* 可选已安装的网络控件选择;null 清除,省略时保持当前设置。
|
|
@@ -545,7 +545,7 @@ export interface SetAppProfileParams {
|
|
|
545
545
|
*/
|
|
546
546
|
networkControl?: NetworkControlSelection | null;
|
|
547
547
|
/**
|
|
548
|
-
*
|
|
548
|
+
* 可选设备联动参数。传入时显式字段覆盖端模拟的推荐设备,空对象使用推荐设备且默认开启
|
|
549
549
|
* deviceShell;省略时不设置或清除该 tab 的设备状态。
|
|
550
550
|
*/
|
|
551
551
|
emulation?: BrowserEmulationOptions;
|
|
@@ -580,7 +580,7 @@ export interface PageClickParams {
|
|
|
580
580
|
};
|
|
581
581
|
/** CSS/text 查询的目标 frame;省略时容器模式自动使用当前业务 frame。 */
|
|
582
582
|
frameId?: string;
|
|
583
|
-
/** point
|
|
583
|
+
/** point 的坐标空间;手机壳预览默认 active-frame,其他页面默认 outer-viewport。 */
|
|
584
584
|
coordinateSpace?: 'outer-viewport' | 'active-frame';
|
|
585
585
|
role?: string;
|
|
586
586
|
exact?: boolean;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 两条路径共享 preset 表 / 默认值推导 / 入参校验,确保对外行为完全一致。
|
|
9
9
|
*/
|
|
10
10
|
import type { BrowserDevicePresetName, BrowserEmulationOptions } from './browserActionProtocol.js';
|
|
11
|
+
import type { DeviceChromeViewConfig } from './deviceChromeView.js';
|
|
11
12
|
export interface DevicePreset {
|
|
12
13
|
width: number;
|
|
13
14
|
height: number;
|
|
@@ -43,6 +44,11 @@ export type DeviceCutout = 'none' | 'dynamic-island' | 'notch' | 'side-camera';
|
|
|
43
44
|
export interface DeviceAppearance {
|
|
44
45
|
cutout: DeviceCutout;
|
|
45
46
|
}
|
|
47
|
+
export interface ResolvedDeviceChromeConfig extends DeviceChromeViewConfig {
|
|
48
|
+
safeAreaInsets: SafeAreaInsets;
|
|
49
|
+
}
|
|
50
|
+
/** Shared system-bar geometry for H5 containers and local mini-programs. */
|
|
51
|
+
export declare function getDeviceChromeViewConfig(emulation: ResolvedEmulationConfig): ResolvedDeviceChromeConfig;
|
|
46
52
|
/** Duo 除内屏竖屏外使用纵向系统栏,业务 WebView 避让屏幕右边缘。 */
|
|
47
53
|
export declare function getDeviceSideBarWidth(preset: BrowserDevicePresetName | undefined, orientation: 'portrait' | 'landscape'): number;
|
|
48
54
|
export declare const DEVICE_PRESETS: Record<BrowserDevicePresetName, DevicePreset>;
|
package/dist/deviceEmulation.js
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 两条路径共享 preset 表 / 默认值推导 / 入参校验,确保对外行为完全一致。
|
|
9
9
|
*/
|
|
10
|
+
/** Shared system-bar geometry for H5 containers and local mini-programs. */
|
|
11
|
+
export function getDeviceChromeViewConfig(emulation) {
|
|
12
|
+
const platform = emulation.platform === 'Android' || emulation.platform === 'OpenHarmony' ? 'android' : 'ios';
|
|
13
|
+
const sideBarWidth = getDeviceSideBarWidth(emulation.presetName, emulation.orientation);
|
|
14
|
+
return {
|
|
15
|
+
platform,
|
|
16
|
+
statusBarHeight: sideBarWidth ? 0 : emulation.orientation === 'landscape' ? 24 : emulation.safeAreaInsets.top,
|
|
17
|
+
bottomBarHeight: emulation.safeAreaInsets.bottom,
|
|
18
|
+
...(sideBarWidth ? { sideBarWidth } : {}),
|
|
19
|
+
safeAreaInsets: { ...emulation.safeAreaInsets },
|
|
20
|
+
cutout: platform === 'ios' ? emulation.appearance.cutout : 'none',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
10
23
|
const IPHONE_DUO_SIDE_BAR_WIDTH = 84;
|
|
11
24
|
/** Duo 除内屏竖屏外使用纵向系统栏,业务 WebView 避让屏幕右边缘。 */
|
|
12
25
|
export function getDeviceSideBarWidth(preset, orientation) {
|
|
@@ -180,6 +180,12 @@ export interface HostToExtensionActionMap {
|
|
|
180
180
|
request: Record<string, unknown>;
|
|
181
181
|
response: unknown;
|
|
182
182
|
};
|
|
183
|
+
__profile_install: {
|
|
184
|
+
request: {
|
|
185
|
+
source: string;
|
|
186
|
+
};
|
|
187
|
+
response: unknown;
|
|
188
|
+
};
|
|
183
189
|
__preset_browser: {
|
|
184
190
|
request: {
|
|
185
191
|
operation: 'sync';
|
|
@@ -257,6 +263,7 @@ export interface HostToExtensionActionMap {
|
|
|
257
263
|
export declare const HOST_TO_EXTENSION_ACTIONS: {
|
|
258
264
|
readonly __browser_enhancement: true;
|
|
259
265
|
readonly __preset_installation: true;
|
|
266
|
+
readonly __profile_install: true;
|
|
260
267
|
readonly __preset_browser: true;
|
|
261
268
|
readonly __export_cookies: true;
|
|
262
269
|
readonly __capture_download: true;
|
|
@@ -30,6 +30,7 @@ export function isExtensionUiServiceAction(value) {
|
|
|
30
30
|
export const HOST_TO_EXTENSION_ACTIONS = {
|
|
31
31
|
__browser_enhancement: true,
|
|
32
32
|
__preset_installation: true,
|
|
33
|
+
__profile_install: true,
|
|
33
34
|
__preset_browser: true,
|
|
34
35
|
__export_cookies: true,
|
|
35
36
|
__capture_download: true,
|
|
@@ -90,6 +91,13 @@ export function getExtensionServiceTimeout(action, data = {}) {
|
|
|
90
91
|
return 40000;
|
|
91
92
|
if (action === 'agent_chat_pick_directory')
|
|
92
93
|
return 300000;
|
|
94
|
+
if (action === 'agent_chat_install_provider')
|
|
95
|
+
return 420000;
|
|
96
|
+
if (action === 'agent_chat_login_qoder')
|
|
97
|
+
return 60000;
|
|
98
|
+
// Installation can prepare a plugin's runtime after the Skill manager returns.
|
|
99
|
+
if (action === 'agent_skills_install' || action === 'agent_skills_update')
|
|
100
|
+
return 600000;
|
|
93
101
|
if (action.startsWith('agent_skills_'))
|
|
94
102
|
return 130000;
|
|
95
103
|
if ([
|
package/dist/pluginPageHost.js
CHANGED
|
@@ -125,8 +125,9 @@ export class PluginPageRuntimeHost {
|
|
|
125
125
|
if (expectedDocumentId !== undefined && typeof expectedDocumentId !== 'string')
|
|
126
126
|
throw new Error('Invalid plugin page document identity');
|
|
127
127
|
const target = await this.adapter.resolve(Number(tabId), frame, expectedDocumentId);
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
const url = new URL(target.url);
|
|
129
|
+
if (!['http:', 'https:'].includes(url.protocol) && (url.protocol !== 'file:' || url.hostname || frame !== 'main'))
|
|
130
|
+
throw new Error('Plugin pages require an HTTP(S) document or a main-frame local preview');
|
|
130
131
|
return { ...target, browserSessionId: this.sessionId };
|
|
131
132
|
}
|
|
132
133
|
async current(record) {
|
|
@@ -650,7 +651,7 @@ export class PluginPageRuntimeHost {
|
|
|
650
651
|
if (!Array.isArray(matches) ||
|
|
651
652
|
!matches.length ||
|
|
652
653
|
matches.length > 64 ||
|
|
653
|
-
!matches.every(isPluginPageMatchPattern) ||
|
|
654
|
+
!matches.every(match => isPluginPageMatchPattern(match) && !(match.startsWith('file:') && match.includes('*'))) ||
|
|
654
655
|
!Array.isArray(data.allowedMatches) ||
|
|
655
656
|
!data.allowedMatches.every(isPluginPageMatchPattern))
|
|
656
657
|
throw new Error('Invalid page activation URL matches');
|
package/dist/pluginPages.d.ts
CHANGED
package/dist/pluginPages.js
CHANGED
|
@@ -20,8 +20,9 @@ export function pluginPageUnavailable(error) {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
const pattern = /^(https?|\*):\/\/(\*|\*\.[a-z0-9.-]+|[a-z0-9.-]+|localhost)(:\d+)?(\/[^#]*)$/i;
|
|
23
|
+
const localPattern = /^file:\/\/\/(?:[^/*?#\\]+\/)+(?:\*|[^/*?#\\]+)\.html$/;
|
|
23
24
|
export function isPluginPageMatchPattern(value) {
|
|
24
|
-
return typeof value === 'string' && value.length <= 2048 && pattern.test(value);
|
|
25
|
+
return typeof value === 'string' && value.length <= 2048 && (pattern.test(value) || localPattern.test(value));
|
|
25
26
|
}
|
|
26
27
|
export function matchesPluginPageURL(matches, value) {
|
|
27
28
|
let url;
|
|
@@ -31,6 +32,14 @@ export function matchesPluginPageURL(matches, value) {
|
|
|
31
32
|
catch {
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
35
|
+
if (url.protocol === 'file:') {
|
|
36
|
+
if (url.hostname || !url.pathname.endsWith('.html'))
|
|
37
|
+
return false;
|
|
38
|
+
url.search = '';
|
|
39
|
+
url.hash = '';
|
|
40
|
+
return matches.some(item => localPattern.test(item) && (item === url.href ||
|
|
41
|
+
(item.endsWith('/*.html') && item.slice(0, item.lastIndexOf('/') + 1) === url.href.slice(0, url.href.lastIndexOf('/') + 1))));
|
|
42
|
+
}
|
|
34
43
|
if (!['http:', 'https:'].includes(url.protocol))
|
|
35
44
|
return false;
|
|
36
45
|
return matches.some(item => {
|
package/dist/proxyPermissions.js
CHANGED
|
@@ -94,6 +94,7 @@ export const ACTIONS_SKIP_DOMAIN_CHECK = new Set([
|
|
|
94
94
|
'__toolbox_open',
|
|
95
95
|
'__extension_reload',
|
|
96
96
|
'__preset_browser',
|
|
97
|
+
'__profile_install',
|
|
97
98
|
// tab_list is a read-only enumeration; sensitive operations on specific
|
|
98
99
|
// tabs (page_eval, send_request, etc.) are still individually checked.
|
|
99
100
|
'tab_list',
|
|
@@ -13,6 +13,41 @@ export function installRootWebViewDocumentTransport() {
|
|
|
13
13
|
// 无效交付地址保留原路径,不导航到未经校验的地址。
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
if (!browser.__mearlRootDocumentUrl__ &&
|
|
17
|
+
browser.parent === browser.top &&
|
|
18
|
+
browser.parent !== browser) {
|
|
19
|
+
const frame = browser.frameElement;
|
|
20
|
+
const syncShellUrl = () => {
|
|
21
|
+
if (location.pathname.startsWith(prefix))
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
const shell = browser.parent;
|
|
25
|
+
if (shell.location.origin !== location.origin || shell.location.href === location.href)
|
|
26
|
+
return false;
|
|
27
|
+
// The iframe navigation already owns the history entry. Mirror its URL without adding another.
|
|
28
|
+
shell.history.replaceState(shell.history.state, '', location.href);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// The shell may be navigating or have an inaccessible origin.
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
if (frame?.hasAttribute('data-mearl-root-webview')) {
|
|
37
|
+
const animationId = 'mearl-root-webview-enter';
|
|
38
|
+
frame.getAnimations().find(animation => animation.id === animationId)?.cancel();
|
|
39
|
+
if (syncShellUrl() && !browser.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
40
|
+
frame.animate([{ opacity: 0.75 }, { opacity: 1 }], {
|
|
41
|
+
duration: 160,
|
|
42
|
+
easing: 'ease-out',
|
|
43
|
+
}).id = animationId;
|
|
44
|
+
}
|
|
45
|
+
browser.addEventListener('pageshow', event => {
|
|
46
|
+
if (event.persisted)
|
|
47
|
+
syncShellUrl();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
16
51
|
browser.__mearlRootDocumentUrl__ = (url) => {
|
|
17
52
|
const navigation = performance.getEntriesByType('navigation')[0];
|
|
18
53
|
const token = navigation?.serverTiming?.find(entry => entry.name === 'mearl-document')?.description;
|
|
@@ -29,10 +29,12 @@ export interface WebViewDocumentResponse {
|
|
|
29
29
|
}
|
|
30
30
|
export interface RootWebViewNavigationOptions {
|
|
31
31
|
client: WebViewNavigationClient;
|
|
32
|
-
shellHeaders(): Promise<Array<{
|
|
32
|
+
shellHeaders(forDocument: boolean): Promise<Array<{
|
|
33
33
|
name: string;
|
|
34
34
|
value: string;
|
|
35
35
|
}> | undefined>;
|
|
36
|
+
/** The host supports loading same-origin documents directly in the root frame. */
|
|
37
|
+
keepSameOriginNavigation?: boolean;
|
|
36
38
|
/** 开发代理把业务路径保存在外层 origin 中,不迁移该 origin。 */
|
|
37
39
|
preserveOrigin?(current: URL, destination: URL): Promise<boolean> | boolean;
|
|
38
40
|
}
|
|
@@ -40,6 +42,7 @@ export interface WebViewFrameTree {
|
|
|
40
42
|
frame: {
|
|
41
43
|
id: string;
|
|
42
44
|
url: string;
|
|
45
|
+
urlFragment?: string;
|
|
43
46
|
name?: string;
|
|
44
47
|
parentId?: string;
|
|
45
48
|
};
|
|
@@ -52,13 +55,17 @@ export interface WebViewFrameEvent {
|
|
|
52
55
|
reason?: string;
|
|
53
56
|
}
|
|
54
57
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
58
|
+
* 根 WebView 在现有外壳内完成同源导航。顶层导航的 HTML 响应通过内部地址
|
|
59
|
+
* 一次性交付给根 WebView,避免重放登录回调;非 HTML 和下载保持浏览器语义。
|
|
57
60
|
*/
|
|
58
61
|
export declare function createRootWebViewNavigation(options: RootWebViewNavigationOptions): {
|
|
62
|
+
isSameOriginRootNavigation: (params: WebViewNavigationRequest) => boolean;
|
|
63
|
+
preserveNavigationSource: ({ request }: WebViewNavigationRequest) => (() => void);
|
|
64
|
+
allowLocalReload(frameId: string, url: string): () => void;
|
|
59
65
|
dispose(): void;
|
|
60
66
|
initialize(tree: WebViewFrameTree): void;
|
|
61
|
-
|
|
67
|
+
/** Returns the committed business URL when the root navigates within the current shell. */
|
|
68
|
+
handleFrameEvent(method: string, params: WebViewFrameEvent): string | undefined;
|
|
62
69
|
/** Mock 与真实响应共用交付入口,保持浏览器 URL 和一次性文档语义一致。 */
|
|
63
70
|
fulfillResponse(params: WebViewNavigationRequest, response: WebViewDocumentResponse, requestClient?: WebViewNavigationClient): Promise<boolean>;
|
|
64
71
|
handle(params: WebViewNavigationRequest, requestClient?: WebViewNavigationClient): Promise<boolean>;
|
|
@@ -49,13 +49,15 @@ async function readDocumentBody(client, requestId) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* 根 WebView 在现有外壳内完成同源导航。顶层导航的 HTML 响应通过内部地址
|
|
53
|
+
* 一次性交付给根 WebView,避免重放登录回调;非 HTML 和下载保持浏览器语义。
|
|
54
54
|
*/
|
|
55
55
|
export function createRootWebViewNavigation(options) {
|
|
56
56
|
let handoff;
|
|
57
|
+
let localReload;
|
|
57
58
|
let mainFrame;
|
|
58
59
|
let rootFrameId;
|
|
60
|
+
let committedRoot;
|
|
59
61
|
const directChildFrames = new Set();
|
|
60
62
|
let navigationHops = 0;
|
|
61
63
|
let document;
|
|
@@ -67,6 +69,27 @@ export function createRootWebViewNavigation(options) {
|
|
|
67
69
|
clearTimeout(expiry);
|
|
68
70
|
expiry = undefined;
|
|
69
71
|
};
|
|
72
|
+
const isSameOriginRootNavigation = (params) => {
|
|
73
|
+
if (!options.keepSameOriginNavigation || !mainFrame || !committedRoot ||
|
|
74
|
+
params.frameId !== committedRoot.id ||
|
|
75
|
+
params.resourceType !== 'Document' || params.request.method !== 'GET' ||
|
|
76
|
+
params.responseStatusCode != null || params.responseErrorReason != null ||
|
|
77
|
+
!/^https?:/.test(params.request.url) || !/^https?:/.test(mainFrame.url))
|
|
78
|
+
return false;
|
|
79
|
+
const destination = new URL(params.request.url);
|
|
80
|
+
return !destination.pathname.startsWith(DOCUMENT_PREFIX) &&
|
|
81
|
+
!(localReload && localReload.frameId === params.frameId && localReload.url === destination.href) &&
|
|
82
|
+
destination.origin === committedRoot.origin && destination.origin === new URL(mainFrame.url).origin;
|
|
83
|
+
};
|
|
84
|
+
const preserveNavigationSource = ({ request }) => {
|
|
85
|
+
const source = {
|
|
86
|
+
url: new URL(request.url).href,
|
|
87
|
+
headers: Object.fromEntries(Object.entries(request.headers ?? {}).filter(([name]) => /^(referer|origin)$/i.test(name))),
|
|
88
|
+
};
|
|
89
|
+
handoff = source;
|
|
90
|
+
return () => { if (handoff === source)
|
|
91
|
+
handoff = undefined; };
|
|
92
|
+
};
|
|
70
93
|
const deliverDocument = async (params, requestClient, headers, readBody) => {
|
|
71
94
|
const destination = new URL(params.request.url);
|
|
72
95
|
const originalHeaders = params.responseHeaders ?? [];
|
|
@@ -127,35 +150,62 @@ export function createRootWebViewNavigation(options) {
|
|
|
127
150
|
return true;
|
|
128
151
|
};
|
|
129
152
|
return {
|
|
153
|
+
isSameOriginRootNavigation,
|
|
154
|
+
preserveNavigationSource,
|
|
155
|
+
allowLocalReload(frameId, url) {
|
|
156
|
+
const destination = new URL(url);
|
|
157
|
+
destination.hash = '';
|
|
158
|
+
const reload = { frameId, url: destination.href };
|
|
159
|
+
localReload = reload;
|
|
160
|
+
return () => { if (localReload === reload)
|
|
161
|
+
localReload = undefined; };
|
|
162
|
+
},
|
|
130
163
|
dispose() {
|
|
131
164
|
disposed = true;
|
|
132
165
|
deliveryGeneration += 1;
|
|
133
166
|
clearDocument();
|
|
134
167
|
handoff = undefined;
|
|
168
|
+
localReload = undefined;
|
|
135
169
|
directChildFrames.clear();
|
|
136
170
|
},
|
|
137
171
|
initialize(tree) {
|
|
138
172
|
mainFrame = tree.frame;
|
|
139
|
-
|
|
140
|
-
|
|
173
|
+
const root = tree.childFrames?.find(child => child.frame.name === '__mearl_root__')?.frame;
|
|
174
|
+
rootFrameId = root?.id;
|
|
175
|
+
committedRoot = root && /^https?:/.test(root.url)
|
|
176
|
+
? { id: root.id, origin: new URL(root.url).origin }
|
|
177
|
+
: undefined;
|
|
141
178
|
directChildFrames.clear();
|
|
142
179
|
for (const child of tree.childFrames ?? [])
|
|
143
180
|
directChildFrames.add(child.frame.id);
|
|
144
181
|
},
|
|
182
|
+
/** Returns the committed business URL when the root navigates within the current shell. */
|
|
145
183
|
handleFrameEvent(method, params) {
|
|
146
184
|
if (method === 'Page.frameNavigated' && params.frame) {
|
|
147
185
|
const frame = params.frame;
|
|
148
186
|
if (!frame.parentId) {
|
|
149
187
|
mainFrame = frame;
|
|
150
188
|
rootFrameId = undefined;
|
|
189
|
+
committedRoot = undefined;
|
|
151
190
|
directChildFrames.clear();
|
|
152
191
|
}
|
|
153
|
-
else if (frame.parentId === mainFrame
|
|
192
|
+
else if (mainFrame && frame.parentId === mainFrame.id) {
|
|
154
193
|
directChildFrames.add(frame.id);
|
|
155
|
-
if (frame.name === '__mearl_root__') {
|
|
194
|
+
if (frame.id === rootFrameId || frame.name === '__mearl_root__') {
|
|
156
195
|
rootFrameId = frame.id;
|
|
157
|
-
if (/^https?:/.test(frame.url))
|
|
196
|
+
if (/^https?:/.test(frame.url)) {
|
|
158
197
|
navigationHops = 0;
|
|
198
|
+
const hasPreviousDocument = committedRoot?.id === frame.id;
|
|
199
|
+
const destination = new URL(frame.url);
|
|
200
|
+
committedRoot = { id: frame.id, origin: destination.origin };
|
|
201
|
+
if (hasPreviousDocument && !destination.pathname.startsWith(DOCUMENT_PREFIX) &&
|
|
202
|
+
/^https?:/.test(mainFrame.url) &&
|
|
203
|
+
destination.origin === new URL(mainFrame.url).origin) {
|
|
204
|
+
if (frame.urlFragment !== undefined)
|
|
205
|
+
destination.hash = frame.urlFragment;
|
|
206
|
+
return destination.href;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
159
209
|
}
|
|
160
210
|
}
|
|
161
211
|
}
|
|
@@ -164,10 +214,14 @@ export function createRootWebViewNavigation(options) {
|
|
|
164
214
|
params.frameId) {
|
|
165
215
|
directChildFrames.add(params.frameId);
|
|
166
216
|
}
|
|
167
|
-
else if (method === 'Page.frameDetached' && params.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
217
|
+
else if (method === 'Page.frameDetached' && params.frameId) {
|
|
218
|
+
if (params.frameId === committedRoot?.id)
|
|
219
|
+
committedRoot = undefined;
|
|
220
|
+
if (params.reason !== 'swap') {
|
|
221
|
+
directChildFrames.delete(params.frameId);
|
|
222
|
+
if (params.frameId === rootFrameId)
|
|
223
|
+
rootFrameId = undefined;
|
|
224
|
+
}
|
|
171
225
|
}
|
|
172
226
|
},
|
|
173
227
|
/** Mock 与真实响应共用交付入口,保持浏览器 URL 和一次性文档语义一致。 */
|
|
@@ -178,7 +232,7 @@ export function createRootWebViewNavigation(options) {
|
|
|
178
232
|
params.frameId !== mainFrame.id ||
|
|
179
233
|
!/^https?:/.test(params.request.url))
|
|
180
234
|
return false;
|
|
181
|
-
const headers = await options.shellHeaders();
|
|
235
|
+
const headers = await options.shellHeaders(true);
|
|
182
236
|
if (!headers || disposed)
|
|
183
237
|
return false;
|
|
184
238
|
return deliverDocument({
|
|
@@ -226,9 +280,14 @@ export function createRootWebViewNavigation(options) {
|
|
|
226
280
|
}
|
|
227
281
|
return true;
|
|
228
282
|
}
|
|
283
|
+
if (!responseStage && params.request.method === 'GET' &&
|
|
284
|
+
params.frameId === localReload?.frameId && destination.href === localReload.url) {
|
|
285
|
+
localReload = undefined;
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
229
288
|
if (!mainFrame)
|
|
230
289
|
return false;
|
|
231
|
-
const headers = await options.shellHeaders();
|
|
290
|
+
const headers = await options.shellHeaders(responseStage && params.frameId === mainFrame.id);
|
|
232
291
|
if (!headers || disposed)
|
|
233
292
|
return false;
|
|
234
293
|
if (params.frameId === mainFrame.id) {
|
|
@@ -255,6 +314,7 @@ export function createRootWebViewNavigation(options) {
|
|
|
255
314
|
return false;
|
|
256
315
|
const current = new URL(mainFrame.url);
|
|
257
316
|
if (!/^https?:$/.test(current.protocol) ||
|
|
317
|
+
isSameOriginRootNavigation(params) ||
|
|
258
318
|
(await options.preserveOrigin?.(current, destination)))
|
|
259
319
|
return false;
|
|
260
320
|
if (navigationHops >= 20)
|
|
@@ -265,17 +325,14 @@ export function createRootWebViewNavigation(options) {
|
|
|
265
325
|
responseHeaders: [{ name: 'Cache-Control', value: 'no-store' }],
|
|
266
326
|
});
|
|
267
327
|
navigationHops += 1;
|
|
268
|
-
|
|
269
|
-
url: destination.href,
|
|
270
|
-
headers: Object.fromEntries(Object.entries(params.request.headers ?? {}).filter(([name]) => /^(referer|origin)$/i.test(name))),
|
|
271
|
-
};
|
|
328
|
+
const clearSource = preserveNavigationSource(params);
|
|
272
329
|
try {
|
|
273
330
|
await options.client.sendCommand('Page.navigate', {
|
|
274
331
|
url: destination.href + (params.request.urlFragment ?? ''),
|
|
275
332
|
});
|
|
276
333
|
}
|
|
277
334
|
catch {
|
|
278
|
-
|
|
335
|
+
clearSource();
|
|
279
336
|
}
|
|
280
337
|
return true;
|
|
281
338
|
},
|
package/dist/toolbox.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/browser-core",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Shared pure-logic utilities for Mearl — usable in both browser extension and Node.js environments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
|
+
"README.en.md",
|
|
51
52
|
"dist",
|
|
52
53
|
"!dist/**/*.map"
|
|
53
54
|
],
|