diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 744d37a49..a958e34a7 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -140,6 +140,7 @@ const requiredMainSnippets = [ '"share.open"', '"share.setTarget"', '"navigation.openNativePage"', + '"app.reloadWebView"', '"app.setTitle"', '"app.setBadgeCount"', '"clipboard.writeText"', @@ -162,6 +163,7 @@ const requiredMainSnippets = [ 'import_image_file_payload', 'set_title', 'set_badge_count', + 'window.reload()', 'read_text()', 'normalize_clipboard_text', 'window.theme()', diff --git a/apps/desktop-shell/src-tauri/src/main.rs b/apps/desktop-shell/src-tauri/src/main.rs index 2c18f0c40..0dde11212 100644 --- a/apps/desktop-shell/src-tauri/src/main.rs +++ b/apps/desktop-shell/src-tauri/src/main.rs @@ -97,6 +97,7 @@ fn capabilities() -> Vec<&'static str> { "share.open", "share.setTarget", "navigation.openNativePage", + "app.reloadWebView", "app.openExternalUrl", "app.setTitle", "app.setBadgeCount", @@ -904,6 +905,13 @@ async fn host_bridge_request( None => failed(request.id, "host_error", "main window not found"), } } + "app.reloadWebView" => match app.get_webview_window("main") { + Some(window) => match window.reload() { + Ok(()) => ok(request.id, json!(true)), + Err(error) => failed(request.id, "host_error", error.to_string()), + }, + None => failed(request.id, "host_error", "main window not found"), + }, "clipboard.writeText" => { let text = match required_string_payload(&request, "text") { Ok(text) => text, @@ -1208,6 +1216,10 @@ mod tests { .as_array() .unwrap() .contains(&json!("navigation.openNativePage"))); + assert!(result["capabilities"] + .as_array() + .unwrap() + .contains(&json!("app.reloadWebView"))); assert!(result["capabilities"] .as_array() .unwrap() diff --git a/apps/desktop-shell/src-tauri/tauri.conf.json b/apps/desktop-shell/src-tauri/tauri.conf.json index cccc34094..fe013d956 100644 --- a/apps/desktop-shell/src-tauri/tauri.conf.json +++ b/apps/desktop-shell/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "build": { "beforeDevCommand": "npm --prefix ../.. run dev:web", "beforeBuildCommand": "npm --prefix ../.. run build:raw && npm run typecheck", - "devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,app.lifecycle,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,app.setBadgeCount,network.status,network.statusChanged,clipboard.writeText,clipboard.readText,file.exportText,file.importText,file.exportImage,file.importImage,file.imageDropped,notification.showLocal", + "devUrl": "http://127.0.0.1:3000/?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,app.lifecycle,share.open,share.setTarget,navigation.openNativePage,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.imageDropped,notification.showLocal", "frontendDist": "../../../dist" }, "app": { @@ -14,7 +14,7 @@ { "create": false, "label": "main", - "url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,app.lifecycle,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,app.setBadgeCount,network.status,network.statusChanged,clipboard.writeText,clipboard.readText,file.exportText,file.importText,file.exportImage,file.importImage,file.imageDropped,notification.showLocal", + "url": "index.html?clientRuntime=native_app&clientType=native_app&hostShell=tauri_desktop&hostPlatform=unknown&hostVersion=0.1.0&bridgeVersion=1&hostCapabilities=host.getRuntime,appearance.getColorScheme,app.lifecycle,share.open,share.setTarget,navigation.openNativePage,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.imageDropped,notification.showLocal", "title": "Genarrative", "width": 1280, "height": 820, diff --git a/apps/mobile-shell/App.tsx b/apps/mobile-shell/App.tsx index 219393325..910c907ab 100644 --- a/apps/mobile-shell/App.tsx +++ b/apps/mobile-shell/App.tsx @@ -58,6 +58,9 @@ export default function App() { openWebViewUrl(url) { setWebUrl(url); }, + reloadWebView() { + webViewRef.current?.reload(); + }, }); return () => configureMobileHostBridgeNavigation(null); diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 335d029a7..baf176513 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -117,6 +117,7 @@ for (const snippet of [ "Linking.addEventListener('url'", 'buildMobileShellUrlFromDeepLink', 'configureMobileHostBridgeNavigation', + 'webViewRef.current?.reload()', 'AppState.addEventListener', 'app.lifecycle', 'network.statusChanged', @@ -184,6 +185,7 @@ for (const snippet of [ 'clipboard.readText', 'notification.showLocal', 'network.status', + 'app.reloadWebView', 'getMobileNetworkStatus', 'Notifications.scheduleNotificationAsync', 'Notifications.setNotificationChannelAsync', @@ -222,6 +224,7 @@ for (const capability of [ 'network.statusChanged', 'navigation.openNativePage', 'navigation.canGoBack', + 'app.reloadWebView', 'app.openExternalUrl', 'clipboard.writeText', 'clipboard.readText', diff --git a/apps/mobile-shell/src/mobileHostBridge.test.ts b/apps/mobile-shell/src/mobileHostBridge.test.ts index 9a7b5dbb8..e206a4e87 100644 --- a/apps/mobile-shell/src/mobileHostBridge.test.ts +++ b/apps/mobile-shell/src/mobileHostBridge.test.ts @@ -293,6 +293,7 @@ describe('handleMobileHostBridgeMessage', () => { 'appearance.getColorScheme', 'host.events', 'app.lifecycle', + 'app.reloadWebView', 'network.status', 'network.statusChanged', 'navigation.canGoBack', @@ -343,6 +344,7 @@ describe('handleMobileHostBridgeMessage', () => { configureMobileHostBridgeNavigation({ allowedOrigin: 'https://app.genarrative.world', openWebViewUrl, + reloadWebView: vi.fn(), }); const response = await send( @@ -361,6 +363,7 @@ describe('handleMobileHostBridgeMessage', () => { configureMobileHostBridgeNavigation({ allowedOrigin: 'https://app.genarrative.world', openWebViewUrl: vi.fn(), + reloadWebView: vi.fn(), }); const response = await send( @@ -386,6 +389,20 @@ describe('handleMobileHostBridgeMessage', () => { expect(failedResponse.error.code).toBe('unsupported_method'); }); + test('app.reloadWebView 刷新移动壳当前 WebView', async () => { + const reloadWebView = vi.fn(); + configureMobileHostBridgeNavigation({ + allowedOrigin: 'https://app.genarrative.world', + openWebViewUrl: vi.fn(), + reloadWebView, + }); + + const response = await send(request('app.reloadWebView')); + + expectOk(response); + expect(reloadWebView).toHaveBeenCalledTimes(1); + }); + test('app.openExternalUrl 只打开允许的外链协议', async () => { const response = await send( request('app.openExternalUrl', { diff --git a/apps/mobile-shell/src/mobileHostBridge.ts b/apps/mobile-shell/src/mobileHostBridge.ts index 6c3fb6273..99c3859b9 100644 --- a/apps/mobile-shell/src/mobileHostBridge.ts +++ b/apps/mobile-shell/src/mobileHostBridge.ts @@ -82,6 +82,7 @@ export const MOBILE_HOST_CAPABILITIES: HostBridgeCapability[] = [ 'share.setTarget', 'navigation.openNativePage', 'navigation.canGoBack', + 'app.reloadWebView', 'app.openExternalUrl', 'network.status', 'network.statusChanged', @@ -109,6 +110,7 @@ export function resolveMobileHostCapabilities(platform = Platform.OS) { export type MobileHostBridgeNavigation = { allowedOrigin: string; openWebViewUrl: (url: string) => void; + reloadWebView: () => void; }; let currentShareTarget: unknown = null; @@ -692,6 +694,15 @@ function openNativePage(payload: unknown) { return true; } +function reloadWebView() { + if (!navigation) { + throw unsupported('app.reloadWebView'); + } + + navigation.reloadWebView(); + return true; +} + async function handleRequest(request: HostBridgeRequest) { switch (request.method) { case 'host.getRuntime': @@ -706,6 +717,8 @@ async function handleRequest(request: HostBridgeRequest) { return ok(request, getColorScheme()); case 'app.openExternalUrl': return ok(request, await openExternalUrl(request.payload)); + case 'app.reloadWebView': + return ok(request, reloadWebView()); case 'network.status': return ok(request, await getMobileNetworkStatus()); case 'clipboard.writeText': diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 7f3f3372f..ff7423001 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2283,3 +2283,10 @@ - 影响范围:`api-server` 资产计费包裹、钱包退款补偿、拼图首图后台生成、`spacetime-module` 拼图 task 表、`spacetime-client` bindings/facade、前端 API request id 复用和后端架构文档。 - 验证方式:`npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run check:spacetime-runtime-access`、`node scripts/check-server-rs-ddd-boundaries.mjs`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml wallet_refund_outbox`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml asset_operation`、`npm run test -- src/services/apiClient.test.ts`、`npm run check:encoding`。 - 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + +## 2026-06-18 原生壳 WebView 刷新能力只保留受控当前页刷新 + +- 背景:Expo 移动壳和 Tauri 桌面壳都需要一个真实的宿主级刷新入口,供 H5 在检测到资源、登录态或运行态需要重新载入时请求宿主刷新当前容器;该能力不能演变成任意 URL 导航或原生 WebView ref 透传。 +- 决策:新增 HostBridge method `app.reloadWebView` 和 H5 facade `reloadHostWebView()`。移动端只调用当前 `react-native-webview` 的 `reload()`,桌面端只调用 Tauri 主 `WebviewWindow.reload()`;该 method 不接受 payload,成功只表示宿主已发起刷新,刷新后当前 H5 上下文会卸载。继续把同源跳转留给 `navigation.openNativePage`,外链离开容器留给 `app.openExternalUrl`。 +- 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`src/services/host-bridge/hostBridge.ts`、`apps/mobile-shell/`、`apps/desktop-shell/`、原生壳能力检查脚本和 HostBridge 架构文档。 +- 验证方式:`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index e20f617c3..902d860aa 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -117,6 +117,7 @@ type HostBridgeEvent = { | `share.setTarget` | 同步当前作品分享目标 | 支持 | 支持 | | `share.open` | 打开分享动作 | 支持系统分享面板 | 复制分享文本到剪贴板 | | `navigation.openNativePage` | 打开受控宿主页 | 支持同源 H5 route | 支持同源 H5 route | +| `app.reloadWebView` | 受控刷新当前 WebView | 支持 WebView reload | 支持主窗口 reload | | `app.lifecycle` | 通知 H5 宿主前后台 / 焦点状态 | 支持 AppState 事件 | 支持窗口 focus / blur 事件 | | `navigation.canGoBack` | 通知 H5 宿主返回栈状态 | 支持事件 | 不声明 | | `app.openExternalUrl` | 用系统浏览器打开外链 | 支持白名单协议 | 支持白名单协议 | @@ -252,7 +253,7 @@ GameBridge 禁止: - iOS / Android 深链打开作品详情、创作页和邀请码。 - 登录和支付先 fallback 到 H5;只把能力边界跑通。 -当前状态:已新增 `apps/mobile-shell/`,通过 Expo development build 运行,`react-native-webview` 加载 H5 URL 并附加 `native_app` 宿主 query。移动壳使用真实品牌图标资产,已接入 `genarrative://` scheme、iOS associated domain 和 Android app link filter,启动和运行时 deep link 只会映射到同源 H5 路径并继续附加 HostBridge 上下文,外域和危险协议回退到默认主站入口。首轮真实能力包括 `host.getRuntime`、`appearance.getColorScheme`、`host.events`、`app.lifecycle`、`network.status`、`network.statusChanged`、`share.open`、`share.setTarget`、`navigation.openNativePage`、`navigation.canGoBack`、`app.openExternalUrl`、`clipboard.writeText`、`clipboard.readText`、`file.exportText`、`file.exportImage`、`file.importImage`、`haptics.impact`、`notification.showLocal` 和 Android 返回键回退;其中 `appearance.getColorScheme` 只读系统配色偏好,不强改 H5 或系统主题;`app.lifecycle` 通过 React Native `AppState` 注入 `active` / `inactive` / `background` 统一状态,供 H5 游戏循环、音频和轮询做真实暂停 / 恢复判断,H5 的 `useHostLifecycleActive()` 会把该事件归一成运行态可播放状态,WebAudio 背景音乐和拼图、抓大鹅等固定玩法 `