收口原生壳能力来源

新增共享 HostBridge capability profile,区分 Expo 移动通用、iOS 额外和 Tauri 桌面能力

移动壳能力清单改为引用共享 profile,入口 URL 类型支持 readonly 能力数组

两端配置检查和原生壳总门禁改为反查共享 profile,桌面 Rust 清单保持运行时镜像

补充 HostBridge profile 单测和 H5 readonly 能力合并类型兼容

同步 Expo/Tauri 壳方案、HostBridge 协议文档和项目决策记录
This commit is contained in:
2026-06-19 02:02:15 +08:00
parent a54210b0d1
commit 75cd4f1abc
12 changed files with 192 additions and 68 deletions
@@ -1,9 +1,14 @@
import { describe, expect, test } from 'vitest';
import {
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
HOST_BRIDGE_PUBLIC_WEB_ORIGIN,
HOST_BRIDGE_PUBLIC_WEB_URL,
HOST_BRIDGE_TAURI_COMMAND,
HOST_BRIDGE_CAPABILITIES,
isHostBridgeMethod,
isHostBridgeCapability,
normalizeHostBridgeBadgeCount,
@@ -90,6 +95,44 @@ describe('HostBridge shared contract helpers', () => {
expect(isHostBridgeCapability(null)).toBe(false);
});
test('原生壳 capability profile 来自共享白名单', () => {
const profiles = [
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
];
for (const profile of profiles) {
expect(profile.every((capability) =>
HOST_BRIDGE_CAPABILITIES.includes(capability),
)).toBe(true);
expect(profile).not.toContain('auth.requestLogin');
expect(profile).not.toContain('payment.request');
}
});
test('移动和桌面 profile 保留真实平台差异', () => {
expect(HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES).toEqual([
'app.setBadgeCount',
]);
expect(HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES).toEqual([
...HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
...HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
]);
expect(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES).toContain(
'file.captureImage',
);
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).toContain('app.setTitle');
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).toContain(
'file.imageDropped',
);
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).not.toContain(
'file.captureImage',
);
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).not.toContain(
'haptics.impact',
);
});
test('归一化 HostBridge request id', () => {
expect(normalizeHostBridgeRequestId(' request-1 ')).toBe('request-1');
expect(normalizeHostBridgeRequestId('')).toBeNull();
+63 -1
View File
@@ -71,12 +71,74 @@ export function isHostBridgeCapability(
);
}
export const HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES: readonly HostBridgeCapability[] = [
'host.getRuntime',
'appearance.getColorScheme',
'host.events',
'app.lifecycle',
'share.open',
'share.setTarget',
'navigation.openNativePage',
'navigation.canGoBack',
'app.reloadWebView',
'app.openExternalUrl',
'network.status',
'network.statusChanged',
'clipboard.writeText',
'clipboard.readText',
'file.exportText',
'file.importText',
'file.exportImage',
'file.importImage',
'file.captureImage',
'file.importAudio',
'file.exportAudio',
'haptics.impact',
'notification.showLocal',
];
export const HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES: readonly HostBridgeCapability[] = [
'app.setBadgeCount',
];
export const HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES: readonly HostBridgeCapability[] = [
...HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
...HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
];
export const HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES: readonly HostBridgeCapability[] = [
'host.getRuntime',
'appearance.getColorScheme',
'host.events',
'app.lifecycle',
'share.open',
'share.setTarget',
'navigation.openNativePage',
'navigation.canGoBack',
'app.reloadWebView',
'app.openExternalUrl',
'app.setTitle',
'app.setBadgeCount',
'network.status',
'network.statusChanged',
'clipboard.writeText',
'clipboard.readText',
'file.exportText',
'file.importText',
'file.exportImage',
'file.importImage',
'file.importAudio',
'file.exportAudio',
'file.imageDropped',
'notification.showLocal',
];
export type HostBridgeRuntimeResult = {
shell: NativeHostShell;
platform: NativeHostPlatform;
hostVersion: string | null;
bridgeVersion: number;
capabilities: HostBridgeCapability[];
capabilities: readonly HostBridgeCapability[];
};
export type HostBridgeRequest<Payload = unknown> = {