收口移动触觉反馈边界
将 Expo 触觉反馈调用收口到 haptics 模块 让 HostBridge dispatch 只负责触觉反馈 payload 分发 同步原生壳结构门禁和架构文档清单
This commit is contained in:
@@ -16,6 +16,8 @@ const clipboardPath = new URL('../src/host-bridge/clipboard.ts', import.meta.url
|
||||
const clipboardSource = fs.readFileSync(clipboardPath, 'utf8');
|
||||
const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
|
||||
const dispatchSource = fs.readFileSync(dispatchPath, 'utf8');
|
||||
const hapticsPath = new URL('../src/host-bridge/haptics.ts', import.meta.url);
|
||||
const hapticsSource = fs.readFileSync(hapticsPath, 'utf8');
|
||||
const notificationsPath = new URL('../src/host-bridge/notifications.ts', import.meta.url);
|
||||
const notificationsSource = fs.readFileSync(notificationsPath, 'utf8');
|
||||
const protocolPath = new URL('../src/host-bridge/protocol.ts', import.meta.url);
|
||||
@@ -78,6 +80,7 @@ const requiredMobileShellSourceModules = [
|
||||
'host-bridge/clipboard.ts',
|
||||
'host-bridge/dispatch.ts',
|
||||
'host-bridge/files.ts',
|
||||
'host-bridge/haptics.ts',
|
||||
'host-bridge/notifications.ts',
|
||||
'host-bridge/protocol.ts',
|
||||
'host-bridge/scanner.ts',
|
||||
@@ -1645,6 +1648,29 @@ if (
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!hapticsSource.includes('normalizeHostBridgeHapticsImpactStyle(rawStyle)') ||
|
||||
!hapticsSource.includes('Haptics.ImpactFeedbackStyle.Heavy') ||
|
||||
!hapticsSource.includes('Haptics.ImpactFeedbackStyle.Medium') ||
|
||||
!hapticsSource.includes('Haptics.ImpactFeedbackStyle.Light') ||
|
||||
!hapticsSource.includes('await Haptics.impactAsync(toExpoImpactStyle(style))')
|
||||
) {
|
||||
throw new Error(
|
||||
'mobile shell haptics bridge must normalize impact style with the shared HostBridge boundary',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
dispatchSource.includes("from 'expo-haptics'") ||
|
||||
dispatchSource.includes('Haptics.impactAsync') ||
|
||||
dispatchSource.includes('Haptics.ImpactFeedbackStyle') ||
|
||||
!dispatchSource.includes('runMobileHapticsImpact(')
|
||||
) {
|
||||
throw new Error(
|
||||
'mobile shell dispatch must delegate haptics IO to haptics.ts',
|
||||
);
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'CameraView',
|
||||
'Camera.requestCameraPermissionsAsync',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import * as Linking from 'expo-linking';
|
||||
import {
|
||||
Appearance,
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
normalizeHostBridgeBadgeCount,
|
||||
normalizeHostBridgeColorScheme,
|
||||
normalizeHostBridgeExternalUrlPayload,
|
||||
normalizeHostBridgeHapticsImpactStyle,
|
||||
normalizeHostBridgeLocalNotification,
|
||||
type OpenExternalUrlPayload,
|
||||
type SetBadgeCountPayload,
|
||||
@@ -54,6 +52,7 @@ import {
|
||||
readMobileClipboardText,
|
||||
writeMobileClipboardText,
|
||||
} from './clipboard';
|
||||
import { runMobileHapticsImpact } from './haptics';
|
||||
|
||||
let currentShareTarget: unknown = null;
|
||||
let navigation: MobileHostBridgeNavigation | null = null;
|
||||
@@ -95,21 +94,14 @@ async function writeClipboard(payload: unknown) {
|
||||
}
|
||||
|
||||
async function runHaptics(payload: unknown) {
|
||||
const style = normalizeHostBridgeHapticsImpactStyle(
|
||||
(payload as HapticsImpactPayload | undefined)?.style,
|
||||
);
|
||||
if (!style) {
|
||||
if (
|
||||
!(await runMobileHapticsImpact(
|
||||
(payload as HapticsImpactPayload | undefined)?.style,
|
||||
))
|
||||
) {
|
||||
throw invalidRequest('haptics impact style must be light, medium, or heavy');
|
||||
}
|
||||
|
||||
const impactStyle =
|
||||
style === 'heavy'
|
||||
? Haptics.ImpactFeedbackStyle.Heavy
|
||||
: style === 'medium'
|
||||
? Haptics.ImpactFeedbackStyle.Medium
|
||||
: Haptics.ImpactFeedbackStyle.Light;
|
||||
|
||||
await Haptics.impactAsync(impactStyle);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
import {
|
||||
normalizeHostBridgeHapticsImpactStyle,
|
||||
type HostBridgeHapticsImpactStyle,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
|
||||
function toExpoImpactStyle(style: HostBridgeHapticsImpactStyle) {
|
||||
return style === 'heavy'
|
||||
? Haptics.ImpactFeedbackStyle.Heavy
|
||||
: style === 'medium'
|
||||
? Haptics.ImpactFeedbackStyle.Medium
|
||||
: Haptics.ImpactFeedbackStyle.Light;
|
||||
}
|
||||
|
||||
export async function runMobileHapticsImpact(rawStyle: unknown) {
|
||||
const style = normalizeHostBridgeHapticsImpactStyle(rawStyle);
|
||||
if (!style) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Haptics.impactAsync(toExpoImpactStyle(style));
|
||||
return true;
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
- 2026-06-19 微信壳路由一致性门禁:`npm run check:native-shells` 必须反查 `miniprogram/app.json.pages`、`miniprogram/host-bridge/protocol.js`、H5 `src/services/host-bridge/hostBridge.ts` 小程序页面常量、H5 `src/services/wechatMiniProgramSubscribe.ts` 订阅授权页面常量、`miniprogram/host-bridge/webView.js` 分享入口 / 分享消息类型、`miniprogram/config.js` source query / 域名格式、`miniprogram/shell/webView.js` 请求头来源标记、H5 runtime parser 和 H5 路由保留字段。新增或调整小程序页面、登录派生 URL、支付页、九宫切图页、订阅页、WebView 来源标记、H5 入口域名、API base URL 或宿主上下文 query 字段时,必须同步这几处常量并保持生产 / 开发域名都显式配置为纯 HTTPS domain;运行时开发域名回退生产域名只作为异常兜底。
|
||||
- 2026-06-18 登录 / 支付能力禁伪声明:`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo / Tauri 壳在真实 SDK、渠道流程和后端契约落地前不得声明这些 capability,也不得把它们写入入口 URL `hostCapabilities`;两端检查脚本会拒绝伪声明,请求实际到达壳层时必须返回明确 `unsupported_method` 并让 H5 fallback,两端壳测试直接覆盖这两个 method。
|
||||
- 2026-06-18 移动壳触觉反馈边界:`haptics.impact` 只接受 `light`、`medium`、`heavy` 三档 impact style,缺省为 `light`;未知值必须返回 `invalid_request`,不得静默降级成真实设备触觉反馈。桌面壳不声明该 capability,H5 继续按 HostBridge fallback 处理。
|
||||
- 2026-06-19 移动壳触觉反馈模块边界:Expo `haptics.impact` 的共享 style 归一、`light` / `medium` / `heavy` 到 `Haptics.ImpactFeedbackStyle` 的映射和真实 `Haptics.impactAsync(...)` 调用统一收口在 `apps/mobile-shell/src/host-bridge/haptics.ts`;`dispatch.ts` 只负责从 HostBridge payload 取 `style` 并调用 `runMobileHapticsImpact(...)`,不得直接导入 `expo-haptics` 或调用 `Haptics.impactAsync`。移动壳配置检查会覆盖该模块结构、共享 style 边界和 dispatch 委托关系,避免触觉反馈能力散落到分发层。
|
||||
- 2026-06-18 分享卡图片导出:新增 `file.exportImage` HostBridge capability,H5 分享卡下载在 native app 中优先把 canvas 生成的 base64 图片交给宿主导出;Expo 壳写缓存图片后交给系统分享 / 保存面板,Tauri 壳通过系统保存对话框写入图片字节。该能力只接受 `image/png` / `image/jpeg` / `image/webp`、单次 5 MiB 内图片数据,成功只返回文件名和字节数,不暴露本机绝对路径;宿主未声明时保留浏览器下载。
|
||||
- 2026-06-18 应用角标能力:新增 `app.setBadgeCount` HostBridge capability,H5 只传 `0` 到共享契约 `HOST_BRIDGE_BADGE_COUNT_MAX` 之间的整数并在宿主未声明时静默 fallback;Expo 壳只在 iOS 声明并通过 React Native `PushNotificationIOS` 设置应用图标角标,Android 不声明、不伪造成功;Tauri 壳通过主窗口 `set_badge_count` 设置任务栏角标,底层平台不支持时返回真实错误。
|
||||
- 2026-06-18 草稿生成未读角标:平台壳层把“可见作品架里未读的草稿生成完成更新”同步到 `app.setBadgeCount`;同一草稿的 work/profile/session 等多个恢复 ID 只计 1,已读、失败、生成中和不可见草稿不计入。该角标只消费已有 HostBridge 能力,宿主不支持或设置失败不影响 H5 红点、作品架或后端状态。
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -255,6 +255,7 @@ const expectedMobileHostBridgeFiles = [
|
||||
'clipboard.ts',
|
||||
'dispatch.ts',
|
||||
'files.ts',
|
||||
'haptics.ts',
|
||||
'notifications.ts',
|
||||
'protocol.ts',
|
||||
'scanner.ts',
|
||||
|
||||
Reference in New Issue
Block a user