收口桌面角标通知响应边界

将 Tauri 角标成功响应包装收口到 badge 模块

将 Tauri 本地通知响应映射收口到 notifications 模块

让桌面 dispatch 只委托角标和通知请求

同步桌面壳配置门禁和共享记忆
This commit is contained in:
2026-06-20 00:17:25 +08:00
parent e51d92f0d5
commit 6ecfc89233
5 changed files with 51 additions and 26 deletions
@@ -2272,6 +2272,8 @@ if (
!desktopHostBridgeDispatchSource.includes(
'set_desktop_app_badge_count(&app, &request)',
) ||
desktopHostBridgeDispatchSource.includes('"app.setBadgeCount" => match') ||
desktopHostBridgeDispatchSource.includes('Ok(()) => ok(request.id, json!(true))') ||
desktopHostBridgeDispatchSource.includes('set_badge_count') ||
desktopHostBridgeDispatchSource.includes('BADGE_COUNT_MAX')
) {
@@ -2281,6 +2283,7 @@ for (const snippet of [
'BADGE_COUNT_MAX',
'fn badge_count_payload',
'set_desktop_app_badge_count',
'ok(request.id.clone(), json!(true))',
'"count must be an integer between 0 and 99999"',
]) {
if (!desktopHostBridgeBadgeSource.includes(snippet)) {
@@ -2315,6 +2318,8 @@ if (
!desktopHostBridgeDispatchSource.includes(
'show_desktop_local_notification(&app, &request)',
) ||
desktopHostBridgeDispatchSource.includes('"notification.showLocal" => match') ||
desktopHostBridgeDispatchSource.includes('Ok(()) => ok(request.id, json!(true))') ||
desktopHostBridgeDispatchSource.includes('app.notification()') ||
desktopHostBridgeDispatchSource.includes('NotificationExt') ||
desktopHostBridgeDispatchSource.includes('PermissionState')
@@ -2332,6 +2337,7 @@ for (const snippet of [
'PermissionState::Granted',
'PermissionState::Denied',
'PermissionState::Prompt | PermissionState::PromptWithRationale',
'ok(request.id.clone(), json!(true))',
'"notification permission denied"',
'app.notification().builder()',
]) {
@@ -1,4 +1,5 @@
use crate::host_bridge::protocol::{failed, HostBridgeRequest, HostBridgeResponse};
use crate::host_bridge::protocol::{failed, ok, HostBridgeRequest, HostBridgeResponse};
use serde_json::json;
use serde_json::Value;
use tauri::Manager;
@@ -32,15 +33,19 @@ fn badge_count_payload(request: &HostBridgeRequest) -> Result<Option<i64>, HostB
pub(crate) fn set_desktop_app_badge_count(
app: &tauri::AppHandle,
request: &HostBridgeRequest,
) -> Result<(), HostBridgeResponse> {
let count = badge_count_payload(request)?;
) -> HostBridgeResponse {
let count = match badge_count_payload(request) {
Ok(count) => count,
Err(response) => return response,
};
let Some(window) = app.get_webview_window("main") else {
return Err(failed(request.id.clone(), "host_error", "main window not found"));
return failed(request.id.clone(), "host_error", "main window not found");
};
window
.set_badge_count(count)
.map_err(|error| failed(request.id.clone(), "host_error", error.to_string()))
match window.set_badge_count(count) {
Ok(()) => ok(request.id.clone(), json!(true)),
Err(error) => failed(request.id.clone(), "host_error", error.to_string()),
}
}
#[cfg(test)]
@@ -63,15 +63,9 @@ pub(super) async fn execute_host_bridge_request(
"file.importAudio" => import_desktop_host_bridge_audio_file(&app, &request).await,
"file.exportAudio" => export_desktop_host_bridge_audio_file(&app, &request).await,
"app.setTitle" => set_desktop_host_bridge_window_title(&app, &request),
"app.setBadgeCount" => match set_desktop_app_badge_count(&app, &request) {
Ok(()) => ok(request.id, json!(true)),
Err(response) => response,
},
"app.setBadgeCount" => set_desktop_app_badge_count(&app, &request),
"network.status" => resolve_desktop_host_bridge_network_status(&request).await,
"notification.showLocal" => match show_desktop_local_notification(&app, &request) {
Ok(()) => ok(request.id, json!(true)),
Err(response) => response,
},
"notification.showLocal" => show_desktop_local_notification(&app, &request),
"share.setTarget" => set_desktop_host_bridge_share_target(&app, &request),
"share.open" => open_desktop_host_bridge_share(&app, &request),
_ => resolve_host_bridge_request(request),
@@ -1,4 +1,5 @@
use crate::host_bridge::protocol::{failed, HostBridgeRequest, HostBridgeResponse};
use crate::host_bridge::protocol::{failed, ok, HostBridgeRequest, HostBridgeResponse};
use serde_json::json;
use serde_json::Value;
use tauri_plugin_notification::{NotificationExt, PermissionState};
@@ -86,25 +87,36 @@ fn desktop_notification_permission_action(
pub(crate) fn show_desktop_local_notification(
app: &tauri::AppHandle,
request: &HostBridgeRequest,
) -> Result<(), HostBridgeResponse> {
let (title, body) = local_notification_payload(request)?;
) -> HostBridgeResponse {
let (title, body) = match local_notification_payload(request) {
Ok(payload) => payload,
Err(response) => return response,
};
let notification_manager = app.notification();
let permission_state = notification_manager
.permission_state()
.map_err(|error| failed(request.id.clone(), "host_error", error.to_string()))?;
.map_err(|error| failed(request.id.clone(), "host_error", error.to_string()));
let permission_state = match permission_state {
Ok(permission_state) => permission_state,
Err(response) => return response,
};
let mut permission_action = desktop_notification_permission_action(permission_state);
if permission_action == DesktopNotificationPermissionAction::Request {
let requested_state = notification_manager
.request_permission()
.map_err(|error| failed(request.id.clone(), "host_error", error.to_string()))?;
.map_err(|error| failed(request.id.clone(), "host_error", error.to_string()));
let requested_state = match requested_state {
Ok(requested_state) => requested_state,
Err(response) => return response,
};
permission_action = desktop_notification_permission_action(requested_state);
}
if permission_action != DesktopNotificationPermissionAction::Show {
return Err(failed(
return failed(
request.id.clone(),
"host_error",
"notification permission denied",
));
);
}
let mut notification = app.notification().builder().title(title);
@@ -112,9 +124,10 @@ pub(crate) fn show_desktop_local_notification(
notification = notification.body(body);
}
notification
.show()
.map_err(|error| failed(request.id.clone(), "host_error", error.to_string()))
match notification.show() {
Ok(()) => ok(request.id.clone(), json!(true)),
Err(error) => failed(request.id.clone(), "host_error", error.to_string()),
}
}
#[cfg(test)]
@@ -2668,7 +2668,7 @@
## 2026-06-18 桌面壳通知权限门禁
- 背景:桌面壳已经声明并实现 `notification.showLocal`,且 Tauri capability 只授权 `allow-host-bridge-request`;但 Rust handler 在清洗 payload 后直接调用 `notification.show()`,没有显式检查系统通知权限,也没有把权限拒绝固定成 HostBridge 失败语义。
- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs` 在发送即时本地通知前先调用 `permission_state()`,已授权才发送;处于 prompt / prompt-with-rationale 时只在 Rust 侧调用 `request_permission()` 后复判;最终未授权返回 `host_error: notification permission denied``dispatch.rs` 只保留 method 委托和 HostBridge 响应映射。桌面壳仍不把 `notification:*` 插件命令加入 capability permissions,不向 H5 暴露 notification 插件 JS guest API、远程推送、定时提醒或通知 token。
- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs` 在发送即时本地通知前先调用 `permission_state()`,已授权才发送;处于 prompt / prompt-with-rationale 时只在 Rust 侧调用 `request_permission()` 后复判;最终未授权返回 `host_error: notification permission denied``notifications.rs` 统一承接 payload 校验、权限检查、系统通知调用和 HostBridge 成功 / 失败响应映射,`dispatch.rs` 只保留 method 委托。桌面壳仍不把 `notification:*` 插件命令加入 capability permissions,不向 H5 暴露 notification 插件 JS guest API、远程推送、定时提醒或通知 token。
- 影响范围:`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs``apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs``apps/desktop-shell/scripts/check-config.mjs`、宿主壳能力统一协议文档、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run desktop-shell:typecheck``npm run desktop-shell:test``npm run check:native-shells``npm run typecheck -- --pretty false``npm run check:encoding``git diff --check`
@@ -2804,3 +2804,10 @@
- 决策:Expo 移动壳新增 `apps/mobile-shell/src/host-bridge/network.ts`,统一承接 `network.status` HostBridge 查询和成功响应包装,并复用 `src/shell/network.ts`Tauri 桌面壳新增 `apps/desktop-shell/src-tauri/src/host_bridge/network.rs`,统一承接 `network.status` HostBridge 查询、成功响应和 `host_error` 失败映射,并复用 `shell::network::resolve_desktop_network_status`。两端 `dispatch` 只保留 method 委托,配置检查会拒绝分发层直接导入 shell network、直接执行 `resolve_desktop_network_status`、包装移动网络成功响应或重新映射桌面网络错误。
- 影响范围:`apps/mobile-shell/src/host-bridge/network.ts``apps/mobile-shell/src/host-bridge/dispatch.ts``apps/mobile-shell/scripts/check-config.mjs``apps/desktop-shell/src-tauri/src/host_bridge/network.rs``apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs``apps/desktop-shell/scripts/check-config.mjs``scripts/check-native-shells.mjs`、宿主壳能力统一协议文档、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run mobile-shell:typecheck``npm run mobile-shell:test -- src/host-bridge/bridge.test.ts``npm run desktop-shell:typecheck``cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml``npm run check:native-shells``npm run check:encoding``git diff --check`
## 2026-06-20 桌面角标与通知响应边界
- 背景:Tauri `app.setBadgeCount``notification.showLocal` 的系统调用已分别收在 `badge.rs``notifications.rs`,但 `dispatch.rs` 仍把 `Result<(), HostBridgeResponse>` 映射成 `ok(true)`,继续让分发层知道能力成功响应形状。
- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/badge.rs``apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs` 统一返回 `HostBridgeResponse`,各自承接 payload 校验、系统调用、成功响应和失败响应映射;`dispatch.rs` 只保留 method 委托。桌面壳配置检查会拒绝 `dispatch.rs` 重新对这两个 method 做 `match` 或包装 `ok(true)`
- 影响范围:`apps/desktop-shell/src-tauri/src/host_bridge/badge.rs``apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs``apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs``apps/desktop-shell/scripts/check-config.mjs``docs/project-memory/shared-memory/decision-log.md`
- 验证方式:`npm run desktop-shell:typecheck``npm run desktop-shell:test``npm run check:native-shells``npm run check:encoding``git diff --check`