收口移动触觉反馈边界

将 Expo 触觉反馈调用收口到 haptics 模块

让 HostBridge dispatch 只负责触觉反馈 payload 分发

同步原生壳结构门禁和架构文档清单
This commit is contained in:
2026-06-19 21:12:41 +08:00
parent 2dcc2fa634
commit 9eeaa6a4e0
7 changed files with 62 additions and 18 deletions
@@ -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',
+6 -14
View File
@@ -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`,不得静默降级成真实设备触觉反馈。桌面壳不声明该 capabilityH5 继续按 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 capabilityH5 分享卡下载在 native app 中优先把 canvas 生成的 base64 图片交给宿主导出;Expo 壳写缓存图片后交给系统分享 / 保存面板,Tauri 壳通过系统保存对话框写入图片字节。该能力只接受 `image/png` / `image/jpeg` / `image/webp`、单次 5 MiB 内图片数据,成功只返回文件名和字节数,不暴露本机绝对路径;宿主未声明时保留浏览器下载。
- 2026-06-18 应用角标能力:新增 `app.setBadgeCount` HostBridge capabilityH5 只传 `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
+1
View File
@@ -255,6 +255,7 @@ const expectedMobileHostBridgeFiles = [
'clipboard.ts',
'dispatch.ts',
'files.ts',
'haptics.ts',
'notifications.ts',
'protocol.ts',
'scanner.ts',