@mearl/app-profiles 3.0.0-beta.1 → 3.0.0-beta.10
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.md +30 -7
- package/README.zh-CN.md +38 -0
- package/dist/environment/app-device.d.ts +24 -0
- package/dist/environment/app-device.js +61 -0
- package/dist/environment/changes.d.ts +28 -0
- package/dist/environment/changes.js +53 -0
- package/dist/environment/document.d.ts +11 -0
- package/dist/environment/document.js +79 -0
- package/dist/environment/identity.d.ts +6 -0
- package/dist/environment/identity.js +12 -0
- package/dist/environment/relay.d.ts +5 -0
- package/dist/environment/relay.js +84 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/metadata.d.ts +2 -2
- package/dist/metadata.js +2 -3
- package/dist/profiles.d.ts +3 -1
- package/dist/profiles.js +17 -42
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
# @mearl/app-profiles
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
The core includes no built-in App profiles. Installed capability packages supply App UAs, versions, bridge objects, settings, scheme routes, business responses, and container implementations. This package validates and compiles declarative profiles and applies setting overrides.
|
|
4
6
|
|
|
5
7
|
```ts
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { resolveAppProfileDefinition, type AppProfile } from '@mearl/app-profiles';
|
|
9
|
+
|
|
10
|
+
export function compileInstalledApp(definition: AppProfile) {
|
|
11
|
+
return resolveAppProfileDefinition(definition, { os: 'android' });
|
|
12
|
+
}
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
Hosts can pass validated dynamic definitions to resolveAppProfile or resolveAppProfileDefinition. The compiler preserves bridge, settings, and pageRuntime descriptors without executing capability code. Definitions and options are copied and bound to a revision; plugin upgrades do not directly alter active documents.
|
|
16
|
+
|
|
17
|
+
resolveAppProfileUpdate, rebuildAppProfileConfig, and updateAppProfileSetting preserve the precedence of explicit options, device appearance, and method overrides. Hosts pass preset defaults; existing user selections take priority. Installing a capability package does not mean its default configuration has been applied.
|
|
18
|
+
|
|
19
|
+
Capability packages declare App behavior and install it into controlled pages. Page code and host handlers are linked through the [capability model](../../docs/capability-model.md).
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
## Agent capability discovery
|
|
22
|
+
|
|
23
|
+
`mearl get_capabilities`, SDK `getCapabilities()`, and MCP `browser_get_capabilities` query the target browser through the existing browser/session routing. By default they return actions, device presets, and App summaries. Pass `{ "app": "package/app" }` to expand bridge methods, adjustable settings, and default versions. A partial or unsupported catalog does not establish that an App is absent; entries with available=false cannot be enabled. Methods are grouped by declaration: responses are fixed replies, noop methods succeed without an implementation, and behaviors simulate behavior. None establishes that a real business operation is available.
|
|
24
|
+
|
|
25
|
+
Each App definition may declare optional `usage` metadata. Browser bundles and Node/Skill plugins use the same format:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"usage": {
|
|
30
|
+
"description": "WebView and bridge behavior simulation",
|
|
31
|
+
"limitations": ["Real device authorization requires the corresponding host capability"],
|
|
32
|
+
"skills": ["example-skill"],
|
|
33
|
+
"documentation": [{ "title": "Usage guide", "url": "https://example.com/docs" }]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
14
37
|
|
|
15
|
-
|
|
38
|
+
usage is limited to 4 KiB; description to 512 characters; and limitations, skills, and documentation to 8 entries each. Documentation URLs must use HTTPS without credentials. Queries read installed metadata only: they do not refresh plugins, open documentation, install or load Skills, or expose bridge scripts, global variable values, or fixed response contents. Skill names are author-provided entry points and do not imply installation. Use set_app_profile/tab_open for general operations and consult companion instructions for specialized workflows.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @mearl/app-profiles
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
核心不内置 App 档案。App UA、版本、桥对象、设置、scheme 路由、业务响应和容器实现均由安装的能力包提供;本包负责声明式档案校验、编译与设置覆盖。
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { resolveAppProfileDefinition, type AppProfile } from '@mearl/app-profiles';
|
|
9
|
+
|
|
10
|
+
export function compileInstalledApp(definition: AppProfile) {
|
|
11
|
+
return resolveAppProfileDefinition(definition, { os: 'android' });
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
宿主可以向 resolveAppProfile 或 resolveAppProfileDefinition 提供已校验的动态定义;编译器保留桥、设置及 pageRuntime 描述,不执行能力代码。定义与选项被复制并绑定 revision,插件升级不直接改写活动文档。
|
|
16
|
+
|
|
17
|
+
resolveAppProfileUpdate、rebuildAppProfileConfig 和 updateAppProfileSetting 保持显式选项、设备外观及方法覆盖的优先级。preset 默认值由宿主传入,用户已有选择继续优先;能力包安装成功不等于默认配置已经应用。
|
|
18
|
+
|
|
19
|
+
App 行为由能力包声明并安装到受控页面;页面代码和宿主处理器通过 [能力包模型](../../docs/capability-model.zh-CN.md) 关联。
|
|
20
|
+
|
|
21
|
+
## Agent 能力发现
|
|
22
|
+
|
|
23
|
+
`mearl get_capabilities`、SDK `getCapabilities()`、MCP `browser_get_capabilities` 查询当前目标浏览器,沿用 browser/session 路由。默认返回动作、设备预设和 App 摘要;传 `{ "app": "package/app" }` 按需展开桥方法、可调设置和默认版本。目录为 partial 或 unsupported 时不能据此断言 App 未安装,available=false 的条目不可启用。方法按声明组分类:responses 为固定回包,noop 为成功空实现,behaviors 为模拟行为;这些都不证明真实业务操作可用。
|
|
24
|
+
|
|
25
|
+
能力包可在每份 App 定义中声明可选 `usage`,浏览器包与 Node/Skill 插件共用同一格式:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"usage": {
|
|
30
|
+
"description": "WebView 与桥接行为模拟",
|
|
31
|
+
"limitations": ["真实设备授权需要对应的宿主能力"],
|
|
32
|
+
"skills": ["example-skill"],
|
|
33
|
+
"documentation": [{ "title": "使用说明", "url": "https://example.com/docs" }]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
usage 最大 4 KiB;description 最多 512 字符,limitations/skills/documentation 各最多 8 项。文档只接受无凭据的 HTTPS URL。查询只读取已安装元数据,不执行插件刷新、不打开文档、不安装或加载 Skill,也不输出桥接脚本、全局变量值及固定回包内容。Skill 名称是作者提供的入口,不代表该 Skill 已安装;通用调用继续使用 set_app_profile/tab_open,特殊流程才读取配套说明。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EnvironmentChange, EnvironmentSnapshot } from './changes.js';
|
|
2
|
+
type Component = 'device' | 'app';
|
|
3
|
+
type Values = {
|
|
4
|
+
device: unknown;
|
|
5
|
+
app: unknown;
|
|
6
|
+
};
|
|
7
|
+
export type AppDeviceChanges<T extends Values> = {
|
|
8
|
+
[K in Component]?: EnvironmentChange<T[K]>;
|
|
9
|
+
};
|
|
10
|
+
export type AppDeviceSnapshots<T extends Values> = {
|
|
11
|
+
[K in Component]: EnvironmentSnapshot<T[K]>;
|
|
12
|
+
};
|
|
13
|
+
export interface AppDeviceEnvironmentHost<T extends Values> {
|
|
14
|
+
/** Validate the composed physical environment as well as reading each component's intent. */
|
|
15
|
+
read(): Promise<AppDeviceSnapshots<T>>;
|
|
16
|
+
save(changes: AppDeviceChanges<T>): Promise<void>;
|
|
17
|
+
matchesRestored<K extends Component>(component: K, baseline: T[K], current: T[K]): boolean;
|
|
18
|
+
}
|
|
19
|
+
/** App and device share physical fields. A coordinated change keeps their original baselines. */
|
|
20
|
+
export declare function changeAppDeviceEnvironment<T extends Values, R>(owner: string, components: readonly Component[], changes: AppDeviceChanges<T>, host: AppDeviceEnvironmentHost<T>, effect: () => Promise<R>, restore?: boolean): Promise<{
|
|
21
|
+
result?: R;
|
|
22
|
+
changes: AppDeviceChanges<T>;
|
|
23
|
+
}>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** App and device share physical fields. A coordinated change keeps their original baselines. */
|
|
2
|
+
export async function changeAppDeviceEnvironment(owner, components, changes, host, effect, restore = false) {
|
|
3
|
+
if (!owner)
|
|
4
|
+
throw new Error('Environment owner is required');
|
|
5
|
+
if (!components.length || new Set(components).size !== components.length || components.some(key => key !== 'device' && key !== 'app'))
|
|
6
|
+
throw new Error('Invalid App/device change components');
|
|
7
|
+
const selected = new Set(components.filter(key => !restore || changes[key]));
|
|
8
|
+
if (!selected.size)
|
|
9
|
+
return { changes };
|
|
10
|
+
const before = await host.read();
|
|
11
|
+
const next = structuredClone({ device: changes.device, app: changes.app });
|
|
12
|
+
for (const key of ['device', 'app']) {
|
|
13
|
+
const change = changes[key];
|
|
14
|
+
if (!change)
|
|
15
|
+
continue;
|
|
16
|
+
if (change.owner !== owner || change.component !== key)
|
|
17
|
+
throw new Error('ENVIRONMENT_OWNER_CONFLICT');
|
|
18
|
+
if (change.phase !== 'active' || !change.applied)
|
|
19
|
+
throw new Error('ENVIRONMENT_UNCONFIRMED');
|
|
20
|
+
if (change.applied.revision !== before[key].revision || change.applied.fingerprint !== before[key].fingerprint) {
|
|
21
|
+
next[key] = { ...change, phase: 'conflict' };
|
|
22
|
+
await host.save(next);
|
|
23
|
+
throw new Error(`ENVIRONMENT_STATE_CONFLICT: ${key} changed after this owner wrote it`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const isChanging = (key) => selected.has(key);
|
|
27
|
+
const stage = (key) => {
|
|
28
|
+
const previous = changes[key];
|
|
29
|
+
if (previous || (!restore && isChanging(key)))
|
|
30
|
+
next[key] = {
|
|
31
|
+
owner, component: key,
|
|
32
|
+
baseline: previous?.baseline ?? structuredClone(before[key]),
|
|
33
|
+
...(previous?.applied ? { applied: previous.applied } : {}),
|
|
34
|
+
phase: restore && isChanging(key) ? 'restoring' : 'pending',
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
stage('device');
|
|
38
|
+
stage('app');
|
|
39
|
+
// One host save covers every owned component affected by the shared UA/container transition.
|
|
40
|
+
await host.save(next);
|
|
41
|
+
const result = await effect();
|
|
42
|
+
const after = await host.read();
|
|
43
|
+
if (restore)
|
|
44
|
+
for (const key of selected) {
|
|
45
|
+
if (!host.matchesRestored(key, next[key].baseline.value, after[key].value))
|
|
46
|
+
throw new Error(`ENVIRONMENT_RESTORE_UNCONFIRMED: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
const commit = (key) => {
|
|
49
|
+
const change = next[key];
|
|
50
|
+
if (!change)
|
|
51
|
+
return;
|
|
52
|
+
if (restore && isChanging(key))
|
|
53
|
+
delete next[key];
|
|
54
|
+
else
|
|
55
|
+
next[key] = { ...change, phase: 'active', applied: structuredClone(after[key]) };
|
|
56
|
+
};
|
|
57
|
+
commit('device');
|
|
58
|
+
commit('app');
|
|
59
|
+
await host.save(next);
|
|
60
|
+
return { result, changes: next };
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type EnvironmentComponent = 'device' | 'app' | 'timezone';
|
|
2
|
+
/** Hosts supply a revision for coordinated writes and an observation for external drift. */
|
|
3
|
+
export interface EnvironmentSnapshot<T> {
|
|
4
|
+
revision: number;
|
|
5
|
+
fingerprint: string;
|
|
6
|
+
value: T;
|
|
7
|
+
}
|
|
8
|
+
export interface EnvironmentChange<T> {
|
|
9
|
+
owner: string;
|
|
10
|
+
component: EnvironmentComponent;
|
|
11
|
+
baseline: EnvironmentSnapshot<T>;
|
|
12
|
+
applied?: EnvironmentSnapshot<T>;
|
|
13
|
+
phase: 'pending' | 'active' | 'restoring' | 'conflict' | 'restored';
|
|
14
|
+
}
|
|
15
|
+
/** Callers serialize this flow with their existing page/environment update queue. */
|
|
16
|
+
export interface EnvironmentChangeHost<T> {
|
|
17
|
+
read(): Promise<EnvironmentSnapshot<T>>;
|
|
18
|
+
save(change: EnvironmentChange<T>): Promise<void>;
|
|
19
|
+
restore(baseline: T): Promise<void>;
|
|
20
|
+
matchesRestored(baseline: EnvironmentSnapshot<T>, current: EnvironmentSnapshot<T>): boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Persist recovery information before the first effect; failed/unknown effects remain pending. */
|
|
23
|
+
export declare function applyEnvironmentChange<T, R>(owner: string, component: EnvironmentComponent, previous: EnvironmentChange<T> | undefined, host: EnvironmentChangeHost<T>, apply: () => Promise<R>): Promise<R>;
|
|
24
|
+
/** Restore only this component and only while both coordinated and observed state still match. */
|
|
25
|
+
export declare function restoreEnvironmentChange<T>(owner: string, change: EnvironmentChange<T>, host: EnvironmentChangeHost<T>): Promise<{
|
|
26
|
+
restored: boolean;
|
|
27
|
+
reason?: 'changed' | 'unconfirmed';
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function matches(expected, current) {
|
|
2
|
+
return expected.revision === current.revision && expected.fingerprint === current.fingerprint;
|
|
3
|
+
}
|
|
4
|
+
async function assertCurrent(change, host) {
|
|
5
|
+
if (change.phase !== 'active' || !change.applied) {
|
|
6
|
+
throw new Error('ENVIRONMENT_UNCONFIRMED: reconcile the previous change before writing again');
|
|
7
|
+
}
|
|
8
|
+
if (!matches(change.applied, await host.read())) {
|
|
9
|
+
await host.save({ ...change, phase: 'conflict' });
|
|
10
|
+
throw new Error('ENVIRONMENT_STATE_CONFLICT: the environment changed after this owner wrote it');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Persist recovery information before the first effect; failed/unknown effects remain pending. */
|
|
14
|
+
export async function applyEnvironmentChange(owner, component, previous, host, apply) {
|
|
15
|
+
if (!owner)
|
|
16
|
+
throw new Error('Environment owner is required');
|
|
17
|
+
if (previous) {
|
|
18
|
+
if (previous.owner !== owner || previous.component !== component)
|
|
19
|
+
throw new Error('ENVIRONMENT_OWNER_CONFLICT');
|
|
20
|
+
await assertCurrent(previous, host);
|
|
21
|
+
}
|
|
22
|
+
const change = {
|
|
23
|
+
owner,
|
|
24
|
+
component,
|
|
25
|
+
baseline: previous?.baseline ?? structuredClone(await host.read()),
|
|
26
|
+
phase: 'pending',
|
|
27
|
+
};
|
|
28
|
+
await host.save(change);
|
|
29
|
+
const result = await apply();
|
|
30
|
+
const applied = structuredClone(await host.read());
|
|
31
|
+
await host.save({ ...change, phase: 'active', applied });
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
/** Restore only this component and only while both coordinated and observed state still match. */
|
|
35
|
+
export async function restoreEnvironmentChange(owner, change, host) {
|
|
36
|
+
if (change.owner !== owner)
|
|
37
|
+
throw new Error('ENVIRONMENT_OWNER_CONFLICT');
|
|
38
|
+
if (change.phase === 'restored')
|
|
39
|
+
return { restored: true };
|
|
40
|
+
if (change.phase !== 'active' || !change.applied)
|
|
41
|
+
return { restored: false, reason: 'unconfirmed' };
|
|
42
|
+
if (!matches(change.applied, await host.read())) {
|
|
43
|
+
await host.save({ ...change, phase: 'conflict' });
|
|
44
|
+
return { restored: false, reason: 'changed' };
|
|
45
|
+
}
|
|
46
|
+
const restoring = { ...change, phase: 'restoring' };
|
|
47
|
+
await host.save(restoring);
|
|
48
|
+
await host.restore(structuredClone(change.baseline.value));
|
|
49
|
+
if (!host.matchesRestored(change.baseline, await host.read()))
|
|
50
|
+
return { restored: false, reason: 'unconfirmed' };
|
|
51
|
+
await host.save({ ...restoring, phase: 'restored' });
|
|
52
|
+
return { restored: true };
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { InjectedAppProfileConfig } from '@mearl/browser-core';
|
|
2
|
+
export interface AppProfileDocumentEnvironmentOptions {
|
|
3
|
+
/** Some hosts prepare globals before the separately installed page runtime. */
|
|
4
|
+
globals: 'always' | 'runtime';
|
|
5
|
+
safeAreaVariables?: boolean;
|
|
6
|
+
restoreHintName?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Self-contained MAIN-world bootstrap; hosts own script registration and removal. */
|
|
9
|
+
export declare function installAppProfileDocumentEnvironment(config: InjectedAppProfileConfig, options: AppProfileDocumentEnvironmentOptions): void;
|
|
10
|
+
/** Self-contained document-start scheduling, including App containers that need a body. */
|
|
11
|
+
export declare function installAppProfileWhenDocumentReady(install: () => void, waitForBody?: boolean): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Self-contained MAIN-world bootstrap; hosts own script registration and removal. */
|
|
2
|
+
export function installAppProfileDocumentEnvironment(config, options) {
|
|
3
|
+
const browser = globalThis;
|
|
4
|
+
const target = browser;
|
|
5
|
+
if (options.restoreHintName) {
|
|
6
|
+
try {
|
|
7
|
+
Object.defineProperty(target, options.restoreHintName, {
|
|
8
|
+
configurable: true, enumerable: false, writable: false, value: true,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
target[options.restoreHintName] = true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const includeGlobals = options.globals === 'always' || Boolean(config.pageRuntime);
|
|
16
|
+
for (const [key, value] of Object.entries(includeGlobals ? config.globals : {})) {
|
|
17
|
+
try {
|
|
18
|
+
target[key] = value;
|
|
19
|
+
}
|
|
20
|
+
catch { /* Frozen page globals keep their original values. */ }
|
|
21
|
+
}
|
|
22
|
+
const insets = config.safeAreaInsets ?? { top: config.statusBarHeight, right: 0, bottom: 0, left: 0 };
|
|
23
|
+
if (includeGlobals) {
|
|
24
|
+
target.__STATUS_BAR_HEIGHT__ = config.statusBarHeight;
|
|
25
|
+
target.__SAFE_AREA_INSET_BOTTOM__ = insets.bottom;
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of [
|
|
28
|
+
['userAgent', config.userAgent],
|
|
29
|
+
['platform', config.navigatorPlatform],
|
|
30
|
+
...(includeGlobals ? [['vendor', 'Apple Computer, Inc.']] : []),
|
|
31
|
+
['appVersion', config.userAgent.replace(/^Mozilla\//, '')],
|
|
32
|
+
['maxTouchPoints', config.maxTouchPoints],
|
|
33
|
+
...(includeGlobals && config.policies?.navigatorLanguage === true ? [['language', config.language]] : []),
|
|
34
|
+
]) {
|
|
35
|
+
try {
|
|
36
|
+
Object.defineProperty(browser.navigator, key, { configurable: true, get: () => value });
|
|
37
|
+
}
|
|
38
|
+
catch { /* Non-configurable native properties keep their original values. */ }
|
|
39
|
+
}
|
|
40
|
+
if (includeGlobals && config.policies?.navigatorLanguage === true) {
|
|
41
|
+
try {
|
|
42
|
+
Object.defineProperty(browser.navigator, 'languages', { configurable: true, get: () => [config.language] });
|
|
43
|
+
}
|
|
44
|
+
catch { /* Non-configurable native properties keep their original values. */ }
|
|
45
|
+
}
|
|
46
|
+
if (options.safeAreaVariables) {
|
|
47
|
+
const apply = () => {
|
|
48
|
+
const root = browser.document.documentElement;
|
|
49
|
+
if (!root)
|
|
50
|
+
return false;
|
|
51
|
+
root.style.setProperty('--safe-area-inset-top', `${insets.top}px`);
|
|
52
|
+
root.style.setProperty('--safe-area-inset-right', `${insets.right}px`);
|
|
53
|
+
root.style.setProperty('--safe-area-inset-bottom', `${insets.bottom}px`);
|
|
54
|
+
root.style.setProperty('--safe-area-inset-left', `${insets.left}px`);
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
if (!apply()) {
|
|
58
|
+
const observer = new browser.MutationObserver(() => { if (apply())
|
|
59
|
+
observer.disconnect(); });
|
|
60
|
+
observer.observe(browser.document, { childList: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Self-contained document-start scheduling, including App containers that need a body. */
|
|
65
|
+
export function installAppProfileWhenDocumentReady(install, waitForBody = false) {
|
|
66
|
+
const browser = globalThis;
|
|
67
|
+
const ready = () => Boolean(browser.document.documentElement && (!waitForBody || browser.document.body));
|
|
68
|
+
if (ready()) {
|
|
69
|
+
install();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const observer = new browser.MutationObserver(() => {
|
|
73
|
+
if (!ready())
|
|
74
|
+
return;
|
|
75
|
+
observer.disconnect();
|
|
76
|
+
install();
|
|
77
|
+
});
|
|
78
|
+
observer.observe(browser.document, { childList: true, subtree: waitForBody });
|
|
79
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DeviceEnvironmentObservation, type ResolvedEmulationConfig } from '@mearl/browser-core';
|
|
2
|
+
/** App UA wins over device UA. Unspecified Client Hints retain the captured browser values. */
|
|
3
|
+
export declare function resolveAppDeviceUserAgent(baseline: Pick<DeviceEnvironmentObservation, 'userAgent' | 'userAgentMetadata'>, device?: ResolvedEmulationConfig | null, appUserAgent?: string): {
|
|
4
|
+
userAgent: string;
|
|
5
|
+
userAgentMetadata: import("@mearl/browser-core").DeviceUserAgentMetadata | null;
|
|
6
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { emptyDeviceUserAgentMetadata } from '@mearl/browser-core';
|
|
2
|
+
/** App UA wins over device UA. Unspecified Client Hints retain the captured browser values. */
|
|
3
|
+
export function resolveAppDeviceUserAgent(baseline, device, appUserAgent) {
|
|
4
|
+
const metadata = baseline.userAgentMetadata;
|
|
5
|
+
return {
|
|
6
|
+
userAgent: appUserAgent ?? device?.userAgent ?? baseline.userAgent,
|
|
7
|
+
userAgentMetadata: appUserAgent ? emptyDeviceUserAgentMetadata() : device && metadata ? {
|
|
8
|
+
...structuredClone(metadata), mobile: device.mobile, platform: device.platform,
|
|
9
|
+
platformVersion: device.platformVersion, architecture: device.architecture, model: device.model,
|
|
10
|
+
} : structuredClone(metadata),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Self-contained MAIN-world transport; host services remain outside the page. */
|
|
2
|
+
export function installAppProfileMessageRelay(bindingName, options = {}) {
|
|
3
|
+
const browser = globalThis;
|
|
4
|
+
const target = globalThis;
|
|
5
|
+
const markerName = options.markerName ?? '__mearlCdpAppProfileRelayInstalled__';
|
|
6
|
+
if (target[markerName])
|
|
7
|
+
return;
|
|
8
|
+
target[markerName] = true;
|
|
9
|
+
browser.addEventListener('message', event => {
|
|
10
|
+
if (event.source !== globalThis)
|
|
11
|
+
return;
|
|
12
|
+
const message = event.data;
|
|
13
|
+
if (!message || message.source !== 'mearl-app-bridge')
|
|
14
|
+
return;
|
|
15
|
+
const requestId = message.requestId;
|
|
16
|
+
if (typeof requestId !== 'number')
|
|
17
|
+
return;
|
|
18
|
+
if (message.type === 'settings' && globalThis.top !== globalThis)
|
|
19
|
+
return;
|
|
20
|
+
if (message.type === 'prepareWebView') {
|
|
21
|
+
browser.postMessage({
|
|
22
|
+
source: 'mearl-content',
|
|
23
|
+
type: 'APP_BRIDGE_PREPARE_WEBVIEW_RESPONSE',
|
|
24
|
+
requestId,
|
|
25
|
+
result: { success: true },
|
|
26
|
+
}, '*');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (message.type !== 'protocol' &&
|
|
30
|
+
message.type !== 'protocolStream' &&
|
|
31
|
+
message.type !== 'userInfo' &&
|
|
32
|
+
message.type !== 'authCode' &&
|
|
33
|
+
message.type !== 'authCodeCancel' &&
|
|
34
|
+
message.type !== 'settings' &&
|
|
35
|
+
message.type !== 'screenshot') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const replyFailure = (reason) => {
|
|
39
|
+
browser.console?.warn('[Mearl] App bridge relay unavailable:', reason);
|
|
40
|
+
if (message.type === 'protocolStream') {
|
|
41
|
+
browser.postMessage({
|
|
42
|
+
source: 'mearl-content',
|
|
43
|
+
type: 'APP_BRIDGE_PROTOCOL_STREAM_EVENT',
|
|
44
|
+
requestId,
|
|
45
|
+
eventType: 'onError',
|
|
46
|
+
data: { error: reason },
|
|
47
|
+
}, '*');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const responseTypes = {
|
|
51
|
+
protocol: 'APP_BRIDGE_PROTOCOL_RESPONSE',
|
|
52
|
+
userInfo: 'APP_BRIDGE_USER_INFO_RESPONSE',
|
|
53
|
+
authCode: 'APP_BRIDGE_AUTH_CODE_RESPONSE',
|
|
54
|
+
screenshot: 'APP_BRIDGE_SCREENSHOT_RESPONSE',
|
|
55
|
+
settings: 'APP_BRIDGE_SETTINGS_RESPONSE',
|
|
56
|
+
};
|
|
57
|
+
browser.postMessage({
|
|
58
|
+
source: 'mearl-content',
|
|
59
|
+
type: responseTypes[String(message.type)],
|
|
60
|
+
requestId,
|
|
61
|
+
result: { success: false, error: reason },
|
|
62
|
+
}, '*');
|
|
63
|
+
};
|
|
64
|
+
if (options.supportedTypes && !options.supportedTypes.includes(String(message.type))) {
|
|
65
|
+
replyFailure('this App bridge operation is not supported by the selected backend');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const binding = target[bindingName];
|
|
69
|
+
if (typeof binding !== 'function') {
|
|
70
|
+
replyFailure('native relay binding is unavailable');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
binding(JSON.stringify({
|
|
75
|
+
type: message.type,
|
|
76
|
+
requestId,
|
|
77
|
+
payload: message.payload,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
replyFailure(error instanceof Error ? error.message : 'native relay call failed');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export * from './profiles.js';
|
|
2
2
|
export * from './settings.js';
|
|
3
3
|
export * from './updates.js';
|
|
4
|
+
export * from './environment/changes.js';
|
|
5
|
+
export * from './environment/document.js';
|
|
6
|
+
export * from './environment/relay.js';
|
|
7
|
+
export * from './environment/app-device.js';
|
|
8
|
+
export * from './environment/identity.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export * from './profiles.js';
|
|
2
2
|
export * from './settings.js';
|
|
3
3
|
export * from './updates.js';
|
|
4
|
+
export * from './environment/changes.js';
|
|
5
|
+
export * from './environment/document.js';
|
|
6
|
+
export * from './environment/relay.js';
|
|
7
|
+
export * from './environment/app-device.js';
|
|
8
|
+
export * from './environment/identity.js';
|
package/dist/metadata.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const BUILTIN_APP_PROFILE_LABELS: Record<
|
|
1
|
+
/** App labels are supplied by installed capability definitions. */
|
|
2
|
+
export declare const BUILTIN_APP_PROFILE_LABELS: Readonly<Record<string, string>>;
|
package/dist/metadata.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
};
|
|
1
|
+
/** App labels are supplied by installed capability definitions. */
|
|
2
|
+
export const BUILTIN_APP_PROFILE_LABELS = Object.freeze({});
|
package/dist/profiles.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ export declare function validateAppBridgeConfig(value: unknown): AppBridgeConfig
|
|
|
8
8
|
export declare function mergeAppBridgeConfigs(...configs: Array<AppBridgeConfig | undefined>): AppBridgeConfig | undefined;
|
|
9
9
|
/** 将声明式方法覆盖编译进运行时动作表,不修改档案的静态数据。 */
|
|
10
10
|
export declare function applyAppBridgeConfig(actions: BridgeActionMap, config?: AppBridgeConfig): BridgeActionMap;
|
|
11
|
-
|
|
11
|
+
/** Built-ins provide UA/version identity; full environments come from capability packages. */
|
|
12
|
+
/** Profiles are supplied by installed capabilities; the core has no App defaults. */
|
|
13
|
+
export declare const APP_PROFILES: Readonly<Partial<Record<BuiltinAppProfileName, AppProfile>>>;
|
|
12
14
|
export interface ResolveAppProfileOptions extends AppProfileOptions {
|
|
13
15
|
os?: AppProfileOs;
|
|
14
16
|
appVersion?: string;
|
package/dist/profiles.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { DEVICE_PRESETS,
|
|
2
|
-
import { BUILTIN_APP_PROFILE_LABELS } from './metadata.js';
|
|
1
|
+
import { DEVICE_PRESETS, isAppBridgeBehavior, appProfileRevision, validateAppProfileDefinition, resolveEmulationConfig, } from '@mearl/browser-core';
|
|
3
2
|
function emptyActions() {
|
|
4
3
|
return {
|
|
5
4
|
open: [],
|
|
@@ -8,7 +7,7 @@ function emptyActions() {
|
|
|
8
7
|
navigationBar: [],
|
|
9
8
|
statusBar: [],
|
|
10
9
|
systemInfo: [],
|
|
11
|
-
|
|
10
|
+
protocol: [],
|
|
12
11
|
kvGet: [],
|
|
13
12
|
kvSet: [],
|
|
14
13
|
kvDelete: [],
|
|
@@ -28,7 +27,7 @@ function mergeActions(...maps) {
|
|
|
28
27
|
out.navigationBar.push(...(m.navigationBar ?? []));
|
|
29
28
|
out.statusBar.push(...(m.statusBar ?? []));
|
|
30
29
|
out.systemInfo.push(...m.systemInfo);
|
|
31
|
-
out.
|
|
30
|
+
out.protocol.push(...m.protocol);
|
|
32
31
|
out.kvGet.push(...(m.kvGet ?? []));
|
|
33
32
|
out.kvSet.push(...(m.kvSet ?? []));
|
|
34
33
|
out.kvDelete.push(...(m.kvDelete ?? []));
|
|
@@ -40,7 +39,6 @@ function mergeActions(...maps) {
|
|
|
40
39
|
}
|
|
41
40
|
return out;
|
|
42
41
|
}
|
|
43
|
-
const APP_BRIDGE_BEHAVIOR_SET = new Set(APP_BRIDGE_BEHAVIORS);
|
|
44
42
|
const BRIDGE_ACTION_ARRAY_KEYS = [
|
|
45
43
|
'open',
|
|
46
44
|
'close',
|
|
@@ -48,7 +46,7 @@ const BRIDGE_ACTION_ARRAY_KEYS = [
|
|
|
48
46
|
'navigationBar',
|
|
49
47
|
'statusBar',
|
|
50
48
|
'systemInfo',
|
|
51
|
-
'
|
|
49
|
+
'protocol',
|
|
52
50
|
'kvGet',
|
|
53
51
|
'kvSet',
|
|
54
52
|
'kvDelete',
|
|
@@ -111,7 +109,7 @@ export function validateAppBridgeConfig(value) {
|
|
|
111
109
|
if (Number(hasBehavior) + Number(hasResponse) + Number(hasNoop) !== 1) {
|
|
112
110
|
throw new Error(`bridgeConfig.methods.${method} must define exactly one of behavior, response, or noop=true`);
|
|
113
111
|
}
|
|
114
|
-
if (hasBehavior && !
|
|
112
|
+
if (hasBehavior && !isAppBridgeBehavior(methodConfig.behavior)) {
|
|
115
113
|
throw new Error(`bridgeConfig.methods.${method} has an unsupported behavior`);
|
|
116
114
|
}
|
|
117
115
|
if (hasResponse && !isJsonValue(methodConfig.response)) {
|
|
@@ -174,16 +172,8 @@ export function applyAppBridgeConfig(actions, config) {
|
|
|
174
172
|
return out;
|
|
175
173
|
}
|
|
176
174
|
/** Built-ins provide UA/version identity; full environments come from capability packages. */
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
...(uaFamily && { uaFamily }), globals: {}, bridges: [], activeEvents: [], actions: emptyActions(), schemes: [] };
|
|
180
|
-
}
|
|
181
|
-
export const APP_PROFILES = {
|
|
182
|
-
fliggy: basicProfile('fliggy', 'LX', '10.0.9', ['AliTrip/{v}', 'WindVane/8.7.0']),
|
|
183
|
-
taobao: basicProfile('taobao', 'TB', '10.65.0', ['WindVane/8.7.0']),
|
|
184
|
-
wechat: basicProfile('wechat', 'WeChat', '8.0.58', [], 'wechatMiniProgram'),
|
|
185
|
-
alipay: { ...basicProfile('alipay', 'AP', '10.8.0', ['AlipayClient/{v}']), policies: { navigatorLanguage: true, defaultLanguage: 'zh-CN' } },
|
|
186
|
-
};
|
|
175
|
+
/** Profiles are supplied by installed capabilities; the core has no App defaults. */
|
|
176
|
+
export const APP_PROFILES = Object.freeze({});
|
|
187
177
|
/** 构造容器 UA。os 默认 ios;uaTokens 中的 {v} 替换为 appVersion */
|
|
188
178
|
export function buildAppProfileUa(profile, options = {}) {
|
|
189
179
|
const os = options.os ??
|
|
@@ -199,36 +189,21 @@ export function buildAppProfileUa(profile, options = {}) {
|
|
|
199
189
|
.replaceAll('{deviceModel}', deviceModel)
|
|
200
190
|
.replaceAll('{language}', options.language ?? profile.policies?.defaultLanguage ?? 'zh-Hans');
|
|
201
191
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
return `Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/${version} NetType/WIFI Language/zh_CN miniProgram`;
|
|
207
|
-
}
|
|
208
|
-
const tokens = profile.uaTokens.map(t => t.replaceAll('{v}', version)).filter(Boolean);
|
|
209
|
-
const tail = `${tokens.length > 0 ? ` ${tokens.join(' ')}` : ''}${profile.policies?.navigatorLanguage ? ` Language/${options.language ?? profile.policies.defaultLanguage ?? 'zh-CN'}` : ''}`;
|
|
210
|
-
if (os === 'android') {
|
|
211
|
-
return `Mozilla/5.0 (Linux; Android 17; ${deviceModel}) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/152.0.0.0 Mobile Safari/537.36 AliApp(${profile.appName}/${version})${tail}`;
|
|
212
|
-
}
|
|
213
|
-
return `Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) AliApp(${profile.appName}/${version})${tail}`;
|
|
192
|
+
return os === 'android'
|
|
193
|
+
? `Mozilla/5.0 (Linux; Android 17; ${deviceModel}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Mobile Safari/537.36`
|
|
194
|
+
: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1';
|
|
214
195
|
}
|
|
215
196
|
/**
|
|
216
197
|
* 解析容器档案为可下发配置。
|
|
217
198
|
* @throws profile 名不存在时抛错,错误信息列出全部可用档案
|
|
218
199
|
*/
|
|
219
200
|
export function listBuiltinAppProfiles() {
|
|
220
|
-
return
|
|
221
|
-
id: definition.name,
|
|
222
|
-
label: definition.label,
|
|
223
|
-
source: 'builtin',
|
|
224
|
-
revision: appProfileRevision(definition),
|
|
225
|
-
definition,
|
|
226
|
-
}));
|
|
201
|
+
return [];
|
|
227
202
|
}
|
|
228
203
|
export function resolveAppProfile(name, options = {}, pinnedDefinition) {
|
|
229
204
|
const definition = pinnedDefinition ?? APP_PROFILES[name];
|
|
230
205
|
if (!definition || definition.name !== name) {
|
|
231
|
-
throw new Error(`Unknown app profile "${name}".
|
|
206
|
+
throw new Error(`Unknown app profile "${name}". Install an App simulation capability first.`);
|
|
232
207
|
}
|
|
233
208
|
return resolveAppProfileDefinition(definition, options);
|
|
234
209
|
}
|
|
@@ -257,7 +232,7 @@ export function resolveAppProfileDefinition(definition, options = {}) {
|
|
|
257
232
|
if (options.language !== undefined &&
|
|
258
233
|
profile.policies?.languages &&
|
|
259
234
|
!profile.policies.languages.includes(options.language)) {
|
|
260
|
-
throw new Error(`Unsupported ${profile.
|
|
235
|
+
throw new Error(`Unsupported ${profile.label} language. Use ${profile.policies.languages.join(', ')}.`);
|
|
261
236
|
}
|
|
262
237
|
const os = options.os ??
|
|
263
238
|
(options.devicePreset ? DEVICE_PRESETS[options.devicePreset]?.appProfileOs : undefined) ??
|
|
@@ -272,10 +247,9 @@ export function resolveAppProfileDefinition(definition, options = {}) {
|
|
|
272
247
|
});
|
|
273
248
|
const deviceModel = options.deviceModel ?? DEVICE_PRESETS[devicePreset].appProfileModel;
|
|
274
249
|
const globals = profile.pageRuntime || profile.bridges.length > 0 || Object.keys(profile.globals).length > 0 ? {
|
|
275
|
-
...profile.globals,
|
|
250
|
+
...Object.fromEntries(Object.entries(profile.globals).map(([key, value]) => [key, value.replaceAll('{deviceModel}', deviceModel).replaceAll('{v}', version)])),
|
|
276
251
|
__APP_NAME__: profile.appName,
|
|
277
252
|
__APP_VERSION__: version,
|
|
278
|
-
__FLI_DEVICE_MODEL__: deviceModel,
|
|
279
253
|
__DEVICE_MODEL__: deviceModel,
|
|
280
254
|
} : {};
|
|
281
255
|
const ua = buildAppProfileUa(profile, { ...options, os, deviceModel });
|
|
@@ -294,6 +268,8 @@ export function resolveAppProfileDefinition(definition, options = {}) {
|
|
|
294
268
|
},
|
|
295
269
|
policies: profile.policies ?? {},
|
|
296
270
|
settings: profile.settings ?? [],
|
|
271
|
+
settingsUi: profile.settingsUi,
|
|
272
|
+
settingsGroup: profile.settingsGroup,
|
|
297
273
|
profile: profile.name,
|
|
298
274
|
label: profile.label,
|
|
299
275
|
globals,
|
|
@@ -319,14 +295,13 @@ export function resolveAppProfileDefinition(definition, options = {}) {
|
|
|
319
295
|
schemes: profile.schemes,
|
|
320
296
|
...(profile.miniProgram && { miniProgram: profile.miniProgram }),
|
|
321
297
|
...(profile.imagePicker && { imagePicker: profile.imagePicker }),
|
|
322
|
-
...(profile.geolocation && { geolocation: profile.geolocation }),
|
|
323
298
|
...(profile.overlay && { overlay: profile.overlay }),
|
|
324
299
|
...(profile.popBeforeOpenParam && { popBeforeOpenParam: profile.popBeforeOpenParam }),
|
|
325
300
|
...(profile.messageEvent && { messageEvent: profile.messageEvent }),
|
|
326
301
|
...(profile.nativeBack && { nativeBack: profile.nativeBack }),
|
|
327
302
|
...(profile.layerUrlProxy && { layerUrlProxy: profile.layerUrlProxy }),
|
|
328
303
|
...(profile.urlSyncHostSuffixes && { urlSyncHostSuffixes: profile.urlSyncHostSuffixes }),
|
|
329
|
-
...(profile.
|
|
304
|
+
...(profile.runtimeOptions && { runtimeOptions: JSON.parse(JSON.stringify(profile.runtimeOptions)) }),
|
|
330
305
|
simulateSystemBars: false,
|
|
331
306
|
externalDeviceChrome: false,
|
|
332
307
|
rootContainer: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/app-profiles",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0-beta.10",
|
|
4
|
+
"description": "Declarative App profile compilation for installed capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "Apache-2.0",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@mearl/browser-core": "3.0.0-beta.
|
|
27
|
+
"@mearl/browser-core": "3.0.0-beta.10"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"typescript": "^5.8.3"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/
|
|
34
|
+
"url": "git+https://github.com/mearlhq/mearl.git",
|
|
35
35
|
"directory": "packages/app-profiles"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|