接入原生壳生命周期事件

新增 app.lifecycle HostBridge 能力与 H5 订阅入口

Expo 壳通过 React Native AppState 注入真实前后台状态

Tauri 壳通过主窗口 focus 和 blur 注入真实激活状态

更新壳能力漂移检查、测试和架构文档
This commit is contained in:
2026-06-18 02:16:47 +08:00
parent 45eec17007
commit 346368f0e7
16 changed files with 299 additions and 6 deletions

View File

@@ -7,6 +7,8 @@ use std::sync::Mutex;
use tauri::Manager;
use tauri::Theme;
use tauri::Url;
use tauri::WebviewWindow;
use tauri::WindowEvent;
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
@@ -81,6 +83,7 @@ fn capabilities() -> Vec<&'static str> {
vec![
"host.getRuntime",
"appearance.getColorScheme",
"app.lifecycle",
"share.open",
"share.setTarget",
"navigation.openNativePage",
@@ -403,6 +406,55 @@ fn write_export_bytes_file(path: PathBuf, bytes: Vec<u8>) -> Result<usize, Strin
Ok(byte_count)
}
fn host_bridge_event_script(event: &str, payload: Value) -> Result<String, serde_json::Error> {
let message = json!({
"bridge": HOST_BRIDGE_PROTOCOL,
"version": HOST_BRIDGE_VERSION,
"event": event,
"payload": payload,
});
let data = serde_json::to_string(&message)?;
let data_literal = serde_json::to_string(&data)?;
Ok(format!(
"window.dispatchEvent(new MessageEvent('message', {{ data: {} }})); true;",
data_literal
))
}
fn emit_desktop_lifecycle_event(
window: &WebviewWindow,
state: &'static str,
focused: bool,
native_state: &'static str,
) -> tauri::Result<()> {
let script = host_bridge_event_script(
"app.lifecycle",
json!({
"state": state,
"focused": focused,
"nativeState": native_state,
}),
)
.map_err(tauri::Error::Json)?;
window.eval(script)
}
fn register_desktop_lifecycle_events(window: &WebviewWindow) {
let lifecycle_window = window.clone();
window.on_window_event(move |event| {
if let WindowEvent::Focused(focused) = event {
let (state, native_state) = if *focused {
("active", "focused")
} else {
("inactive", "blurred")
};
let _ = emit_desktop_lifecycle_event(&lifecycle_window, state, *focused, native_state);
}
});
}
fn payload_string<'a>(value: &'a Value, field: &str) -> Option<&'a str> {
value
.get(field)
@@ -727,7 +779,10 @@ fn main() {
.setup(|app| {
let window_config = app.config().app.windows.get(0).cloned();
if let Some(config) = window_config {
tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.build()?;
let window =
tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.build()?;
register_desktop_lifecycle_events(&window);
let _ = emit_desktop_lifecycle_event(&window, "active", true, "created");
}
Ok(())
})
@@ -763,6 +818,10 @@ mod tests {
.as_array()
.unwrap()
.contains(&json!("appearance.getColorScheme")));
assert!(result["capabilities"]
.as_array()
.unwrap()
.contains(&json!("app.lifecycle")));
assert!(result["capabilities"]
.as_array()
.unwrap()
@@ -833,6 +892,25 @@ mod tests {
assert_eq!(color_scheme_from_theme(Theme::Dark), "dark");
}
#[test]
fn host_bridge_event_script_dispatches_lifecycle_message() {
let script = host_bridge_event_script(
"app.lifecycle",
json!({
"state": "active",
"focused": true,
"nativeState": "focused",
}),
)
.expect("event script");
assert!(script.contains("MessageEvent('message'"));
assert!(script.contains("GenarrativeHostBridge"));
assert!(script.contains("app.lifecycle"));
assert!(script.contains("\\\"state\\\":\\\"active\\\""));
assert!(script.contains("\\\"focused\\\":true"));
}
#[test]
fn external_url_normalization_allows_only_safe_protocols() {
assert_eq!(