From 58fbef8f239bd1d8e8bb7b1a90e41893a7831594 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 19:53:43 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=8A=B6=E6=80=81=E5=A4=B1=E8=B4=A5=E8=A7=82?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面外观角标和窗口标题记录系统异常日志 Rust 单测覆盖稳定错误和日志边界 配置门禁和文档同步窗口状态观测约束 --- apps/desktop-shell/scripts/check-config.mjs | 21 +++++++++++-- .../src-tauri/src/host_bridge/appearance.rs | 30 +++++++++++++++++-- .../src-tauri/src/host_bridge/badge.rs | 26 +++++++++++++++- .../src-tauri/src/host_bridge/title.rs | 30 +++++++++++++++++-- .../shared-memory/decision-log.md | 6 ++++ ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 ++ 6 files changed, 107 insertions(+), 8 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index d3778dc8e..52e348bc2 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -2613,18 +2613,23 @@ if ( for (const snippet of [ 'desktop_appearance_color_scheme', 'appearance_unavailable_response', + 'log_desktop_appearance_failure', + 'desktop appearance failed for {label}: {error}', 'color_scheme_from_theme(theme)', 'window.theme()', + 'log_desktop_appearance_failure("theme", &error.to_string())', + 'log_desktop_appearance_failure("window", "main window unavailable")', '"colorScheme"', '"appearance unavailable"', 'appearance_unavailable_response_is_stable', + 'appearance_failures_are_logged_without_exposing_native_detail', ]) { if (!desktopHostBridgeAppearanceSource.includes(snippet)) { throw new Error(`desktop shell appearance module is missing ${snippet}`); } } if ( - desktopHostBridgeAppearanceSource.includes('error.to_string()') || + desktopHostBridgeAppearanceSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeAppearanceSource.includes('"main window not found"') ) { throw new Error('desktop shell appearance module must hide native window errors'); @@ -2645,17 +2650,22 @@ for (const snippet of [ 'fn badge_count_payload', 'set_desktop_app_badge_count', 'badge_unavailable_response', + 'log_desktop_badge_failure', + 'desktop app badge failed for {label}: {error}', + 'log_desktop_badge_failure("window", "main window unavailable")', + 'log_desktop_badge_failure("set", &error.to_string())', 'ok(request.id.clone(), json!(true))', '"count must be an integer between 0 and 99999"', '"badge unavailable"', 'badge_unavailable_response_is_stable', + 'badge_failures_are_logged_without_exposing_native_detail', ]) { if (!desktopHostBridgeBadgeSource.includes(snippet)) { throw new Error(`desktop shell badge module is missing ${snippet}`); } } if ( - desktopHostBridgeBadgeSource.includes('error.to_string()') || + desktopHostBridgeBadgeSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeBadgeSource.includes('"main window not found"') ) { throw new Error('desktop shell badge module must hide native window errors'); @@ -2678,20 +2688,25 @@ for (const snippet of [ 'normalize_window_title', 'window_title_from_request', 'window_title_unavailable_response', + 'log_desktop_window_title_failure', + 'desktop window title failed for {label}: {error}', 'required_string_payload(request, "title")', 'window.set_title(&title)', + 'log_desktop_window_title_failure("set", &error.to_string())', + 'log_desktop_window_title_failure("window", "main window unavailable")', '"title is required"', '"window title unavailable"', 'window_title_request_rejects_missing_or_invalid_payload', 'window_title_request_trims_and_truncates_shared_boundary', 'window_title_unavailable_response_is_stable', + 'window_title_failures_are_logged_without_exposing_native_detail', ]) { if (!desktopHostBridgeTitleSource.includes(snippet)) { throw new Error(`desktop shell title module is missing ${snippet}`); } } if ( - desktopHostBridgeTitleSource.includes('error.to_string()') || + desktopHostBridgeTitleSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeTitleSource.includes('"main window not found"') ) { throw new Error('desktop shell title module must hide native window errors'); diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs b/apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs index d13f9228d..bf49a2a4e 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs @@ -10,9 +10,15 @@ pub(crate) fn desktop_appearance_color_scheme( match app.get_webview_window("main") { Some(window) => match window.theme() { Ok(theme) => ok_color_scheme(request, color_scheme_from_theme(theme)), - Err(_) => appearance_unavailable_response(request), + Err(error) => { + log_desktop_appearance_failure("theme", &error.to_string()); + appearance_unavailable_response(request) + } }, - None => appearance_unavailable_response(request), + None => { + log_desktop_appearance_failure("window", "main window unavailable"); + appearance_unavailable_response(request) + } } } @@ -29,6 +35,14 @@ fn appearance_unavailable_response(request: &HostBridgeRequest) -> HostBridgeRes failed(request.id.clone(), "host_error", "appearance unavailable") } +fn log_desktop_appearance_failure(label: &str, error: &str) -> bool { + if !error.is_empty() { + eprintln!("desktop appearance failed for {label}: {error}"); + } + + false +} + #[cfg(test)] mod tests { use super::*; @@ -56,4 +70,16 @@ mod tests { assert_eq!(error.code, "host_error"); assert_eq!(error.message, "appearance unavailable"); } + + #[test] + fn appearance_failures_are_logged_without_exposing_native_detail() { + assert!(!log_desktop_appearance_failure( + "theme", + "private appearance detail" + )); + assert!(!log_desktop_appearance_failure( + "window", + "main window unavailable" + )); + } } diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/badge.rs b/apps/desktop-shell/src-tauri/src/host_bridge/badge.rs index c518b3cae..c72b6d88e 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/badge.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/badge.rs @@ -39,12 +39,16 @@ pub(crate) fn set_desktop_app_badge_count( Err(response) => return response, }; let Some(window) = app.get_webview_window("main") else { + log_desktop_badge_failure("window", "main window unavailable"); return badge_unavailable_response(request); }; match window.set_badge_count(count) { Ok(()) => ok(request.id.clone(), json!(true)), - Err(_) => badge_unavailable_response(request), + Err(error) => { + log_desktop_badge_failure("set", &error.to_string()); + badge_unavailable_response(request) + } } } @@ -52,6 +56,14 @@ fn badge_unavailable_response(request: &HostBridgeRequest) -> HostBridgeResponse failed(request.id.clone(), "host_error", "badge unavailable") } +fn log_desktop_badge_failure(label: &str, error: &str) -> bool { + if !error.is_empty() { + eprintln!("desktop app badge failed for {label}: {error}"); + } + + false +} + #[cfg(test)] mod tests { use super::*; @@ -94,4 +106,16 @@ mod tests { assert_eq!(error.code, "host_error"); assert_eq!(error.message, "badge unavailable"); } + + #[test] + fn badge_failures_are_logged_without_exposing_native_detail() { + assert!(!log_desktop_badge_failure( + "set", + "private badge detail" + )); + assert!(!log_desktop_badge_failure( + "window", + "main window unavailable" + )); + } } diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/title.rs b/apps/desktop-shell/src-tauri/src/host_bridge/title.rs index 23438947a..e4664059f 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/title.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/title.rs @@ -36,9 +36,15 @@ pub(crate) fn set_desktop_host_bridge_window_title( match app.get_webview_window("main") { Some(window) => match window.set_title(&title) { Ok(()) => ok(request.id.clone(), json!(true)), - Err(_) => window_title_unavailable_response(request), + Err(error) => { + log_desktop_window_title_failure("set", &error.to_string()); + window_title_unavailable_response(request) + } }, - None => window_title_unavailable_response(request), + None => { + log_desktop_window_title_failure("window", "main window unavailable"); + window_title_unavailable_response(request) + } } } @@ -46,6 +52,14 @@ fn window_title_unavailable_response(request: &HostBridgeRequest) -> HostBridgeR failed(request.id.clone(), "host_error", "window title unavailable") } +fn log_desktop_window_title_failure(label: &str, error: &str) -> bool { + if !error.is_empty() { + eprintln!("desktop window title failed for {label}: {error}"); + } + + false +} + #[cfg(test)] mod tests { use super::*; @@ -112,4 +126,16 @@ mod tests { assert_eq!(error.code, "host_error"); assert_eq!(error.message, "window title unavailable"); } + + #[test] + fn window_title_failures_are_logged_without_exposing_native_detail() { + assert!(!log_desktop_window_title_failure( + "set", + "private title detail" + )); + assert!(!log_desktop_window_title_failure( + "window", + "main window unavailable" + )); + } } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 74c4b0a63..b0e147419 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3130,6 +3130,12 @@ - 决策:`apps/desktop-shell/src-tauri/src/host_bridge/files.rs` 必须在导出路径转换、导出写入、导入路径转换和导入后台读取 join 失败时记录 `desktop file export failed for ...` 或 `desktop file import failed for ...` 日志。HostBridge 对 H5 仍只返回稳定错误语义,不透传本地路径、系统错误或线程细节;用户取消系统文件对话框仍返回 `cancelled`,不记录为异常。 - 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::files`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-20 桌面壳窗口状态小能力系统异常必须可观测 + +- 背景:Tauri 桌面壳的外观、角标和窗口标题能力都依赖主窗口和平台系统 API;这些能力对 H5 必须保持稳定错误语义,但开发侧也需要知道是主窗口缺失、主题读取失败还是系统 API 调用失败。 +- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/badge.rs` 和 `apps/desktop-shell/src-tauri/src/host_bridge/title.rs` 必须在主窗口缺失、主题读取失败、角标设置失败和窗口标题设置失败时分别记录 `desktop appearance failed for ...`、`desktop app badge failed for ...` 或 `desktop window title failed for ...` 日志。HostBridge 对 H5 仍只返回稳定 `appearance unavailable`、`badge unavailable` 或 `window title unavailable`,不透传系统错误、窗口内部信息或平台细节。 +- 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::appearance`、`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::badge`、`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::title`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-20 移动壳门禁脚本必须自登记自扫描 - 背景:Expo 移动壳单端检查已把 `apps/mobile-shell/scripts/` 纳入生产源码扫描入口,但 `check-config.mjs` 自身仍被排除在脚本清单和替身词扫描之外;这会让移动壳与桌面壳门禁结构不一致,也可能让后续门禁反查内容绕过生产替身词规则。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 7d226ee67..3681174d3 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -350,6 +350,8 @@ GameBridge 禁止: 2026-06-20 追加:桌面壳文件导入导出的系统异常必须可观测。Tauri dialog 路径转换失败、后台文件读写任务失败或任务 join 失败时,桌面壳必须分别记录 `desktop file export failed for ...` 或 `desktop file import failed for ...` 日志;HostBridge 回包仍只暴露稳定 `file export unavailable` 或 `file import unavailable` 语义,不透传本地路径、系统错误或线程细节。用户取消系统文件对话框仍返回 `cancelled`,不记录为异常。 +2026-06-20 追加:桌面壳窗口状态小能力的系统异常必须可观测。Tauri 主窗口缺失、主题读取失败、角标设置失败或窗口标题设置失败时,桌面壳必须分别记录 `desktop appearance failed for ...`、`desktop app badge failed for ...` 或 `desktop window title failed for ...` 日志;HostBridge 回包仍只暴露稳定 `appearance unavailable`、`badge unavailable` 或 `window title unavailable` 语义,不透传系统错误、窗口内部信息或平台细节。 + 2026-06-18 追加:H5 账号状态刷新开始消费 `app.reloadWebView`。用户登录成功、退出登录、其它身份边界变化或登录状态异常页点击重新尝试时,`AuthGate` 会优先请求 Expo 壳刷新当前 WebView;宿主未声明或刷新失败时再回退浏览器刷新,避免在移动壳内绕过受控容器刷新入口。 2026-06-18 追加:移动壳 WebView 内容 / 渲染进程终止时复用同一受控刷新路径。iOS `onContentProcessDidTerminate` 和 Android `onRenderProcessGone` 只调用当前 `react-native-webview` 的 `reload()`,不改写 H5 URL、不注入额外脚本、不新增宿主恢复页面,避免系统回收 WebView 进程后留下空白容器。