补齐桌面壳文件失败观测
桌面文件导入导出记录系统异常日志 Rust 单测覆盖稳定错误和日志边界 配置门禁和文档同步桌面文件观测约束
This commit is contained in:
@@ -1111,11 +1111,18 @@ function assertDesktopDialogBoundary(method, functionName, filterLabel, expected
|
||||
}
|
||||
const unavailableResponse =
|
||||
action === 'save'
|
||||
? 'Err(_) => return file_export_unavailable_response(request)'
|
||||
: 'Err(_) => return file_import_unavailable_response(request)';
|
||||
? 'return file_export_unavailable_response(request)'
|
||||
: 'return file_import_unavailable_response(request)';
|
||||
if (!fileFunctionBody.includes(unavailableResponse)) {
|
||||
throw new Error(`desktop shell ${method} must hide native path conversion errors`);
|
||||
}
|
||||
const failureLogger =
|
||||
action === 'save'
|
||||
? 'log_desktop_file_export_failure("path.convert", &error.to_string())'
|
||||
: 'log_desktop_file_import_failure("path.convert", &error.to_string())';
|
||||
if (!fileFunctionBody.includes(failureLogger)) {
|
||||
throw new Error(`desktop shell ${method} must log native path conversion errors`);
|
||||
}
|
||||
if (
|
||||
fileFunctionBody.includes(
|
||||
'return failed(request.id.clone(), "host_error", error.to_string())',
|
||||
@@ -2399,8 +2406,22 @@ const requiredRustHostSnippets = [
|
||||
'file_import_cancelled_response',
|
||||
'file_export_unavailable_response',
|
||||
'file_import_unavailable_response',
|
||||
'log_desktop_file_export_failure',
|
||||
'log_desktop_file_import_failure',
|
||||
'desktop file export failed for {label}: {error}',
|
||||
'desktop file import failed for {label}: {error}',
|
||||
'log_desktop_file_export_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_import_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.text", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.image", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.audio", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.text.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.document.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.image.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.audio.join", &error.to_string())',
|
||||
'desktop_file_cancelled_responses_are_stable',
|
||||
'desktop_file_unavailable_responses_are_stable',
|
||||
'desktop_file_failures_are_logged_without_exposing_native_detail',
|
||||
'"file export cancelled"',
|
||||
'"file import cancelled"',
|
||||
'"file export unavailable"',
|
||||
|
||||
@@ -31,6 +31,22 @@ fn file_import_unavailable_response(request: &HostBridgeRequest) -> HostBridgeRe
|
||||
)
|
||||
}
|
||||
|
||||
fn log_desktop_file_export_failure(label: &str, error: &str) -> bool {
|
||||
if !error.is_empty() {
|
||||
eprintln!("desktop file export failed for {label}: {error}");
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn log_desktop_file_import_failure(label: &str, error: &str) -> bool {
|
||||
if !error.is_empty() {
|
||||
eprintln!("desktop file import failed for {label}: {error}");
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) async fn export_desktop_host_bridge_text_file(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
@@ -50,13 +66,23 @@ pub(crate) async fn export_desktop_host_bridge_text_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return file_export_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("path.convert", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
let export_result =
|
||||
tauri::async_runtime::spawn_blocking(move || write_export_text_file(path, content)).await;
|
||||
let bytes = match export_result {
|
||||
Ok(Ok(bytes)) => bytes,
|
||||
Ok(Err(_)) | Err(_) => return file_export_unavailable_response(request),
|
||||
Ok(Err(error)) => {
|
||||
log_desktop_file_export_failure("write.text", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("write.text.join", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
|
||||
ok(
|
||||
@@ -83,14 +109,20 @@ pub(crate) async fn import_desktop_host_bridge_text_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
let import_result =
|
||||
tauri::async_runtime::spawn_blocking(move || import_text_file_payload(path)).await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(_) => file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.text.join", &error.to_string());
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +143,20 @@ pub(crate) async fn import_desktop_host_bridge_document_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
let import_result =
|
||||
tauri::async_runtime::spawn_blocking(move || import_document_file_payload(path)).await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(_) => file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.document.join", &error.to_string());
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,13 +179,23 @@ pub(crate) async fn export_desktop_host_bridge_image_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return file_export_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("path.convert", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
let export_result =
|
||||
tauri::async_runtime::spawn_blocking(move || write_export_bytes_file(path, bytes)).await;
|
||||
let byte_count = match export_result {
|
||||
Ok(Ok(byte_count)) => byte_count,
|
||||
Ok(Err(_)) | Err(_) => return file_export_unavailable_response(request),
|
||||
Ok(Err(error)) => {
|
||||
log_desktop_file_export_failure("write.image", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("write.image.join", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
ok(
|
||||
request.id.clone(),
|
||||
@@ -173,7 +221,10 @@ pub(crate) async fn import_desktop_host_bridge_image_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
let import_result = tauri::async_runtime::spawn_blocking(move || {
|
||||
import_image_file_payload(path, "selected", None)
|
||||
@@ -182,7 +233,10 @@ pub(crate) async fn import_desktop_host_bridge_image_file(
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(_) => file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.image.join", &error.to_string());
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,14 +254,20 @@ pub(crate) async fn import_desktop_host_bridge_audio_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
let import_result =
|
||||
tauri::async_runtime::spawn_blocking(move || import_audio_file_payload(path)).await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(_) => file_import_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.audio.join", &error.to_string());
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,13 +290,23 @@ pub(crate) async fn export_desktop_host_bridge_audio_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => return file_export_unavailable_response(request),
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("path.convert", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
let export_result =
|
||||
tauri::async_runtime::spawn_blocking(move || write_export_bytes_file(path, bytes)).await;
|
||||
let byte_count = match export_result {
|
||||
Ok(Ok(byte_count)) => byte_count,
|
||||
Ok(Err(_)) | Err(_) => return file_export_unavailable_response(request),
|
||||
Ok(Err(error)) => {
|
||||
log_desktop_file_export_failure("write.audio", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("write.audio.join", &error.to_string());
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
ok(
|
||||
request.id.clone(),
|
||||
@@ -286,4 +356,24 @@ mod tests {
|
||||
assert_eq!(import_error.code, "host_error");
|
||||
assert_eq!(import_error.message, "file import unavailable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_file_failures_are_logged_without_exposing_native_detail() {
|
||||
assert!(!log_desktop_file_export_failure(
|
||||
"path.convert",
|
||||
"private export detail"
|
||||
));
|
||||
assert!(!log_desktop_file_export_failure(
|
||||
"write.text",
|
||||
"private export detail"
|
||||
));
|
||||
assert!(!log_desktop_file_import_failure(
|
||||
"path.convert",
|
||||
"private import detail"
|
||||
));
|
||||
assert!(!log_desktop_file_import_failure(
|
||||
"read.text.join",
|
||||
"private import detail"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3124,6 +3124,12 @@
|
||||
- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs` 必须在剪贴板写入 / 读取失败时记录 `desktop clipboard failed for ...` 日志;`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs` 必须在通知权限状态读取、权限请求和通知投递失败时记录 `desktop notification failed for ...` 日志。HostBridge 对 H5 仍只返回稳定错误语义;该约束不新增遥测 SDK、后台通知、自动更新或 H5 直连 Tauri JS 插件。
|
||||
- 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::clipboard host_bridge::notifications`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-06-20 桌面壳文件能力系统异常必须可观测
|
||||
|
||||
- 背景:Tauri 桌面壳已声明文本 / 文档 / 图片 / 音频导入导出;这些能力会打开系统文件对话框、转换系统路径并在后台线程读写文件。如果系统路径转换、后台读写或任务 join 异常只被折叠成稳定 HostBridge 错误,H5 语义安全,但开发侧无法区分系统对话框路径异常、文件系统失败或后台任务失败。
|
||||
- 决策:`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 移动壳门禁脚本必须自登记自扫描
|
||||
|
||||
- 背景:Expo 移动壳单端检查已把 `apps/mobile-shell/scripts/` 纳入生产源码扫描入口,但 `check-config.mjs` 自身仍被排除在脚本清单和替身词扫描之外;这会让移动壳与桌面壳门禁结构不一致,也可能让后续门禁反查内容绕过生产替身词规则。
|
||||
|
||||
@@ -348,6 +348,8 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-20 追加:桌面壳剪贴板和本地通知插件异常必须可观测。Tauri clipboard-manager 读写失败、notification permission state / request 失败、系统通知 show 失败时,桌面壳必须分别记录 `desktop clipboard failed for ...` 或 `desktop notification failed for ...` 日志;HostBridge 回包仍只暴露稳定 `clipboard write unavailable`、`clipboard read unavailable`、`notification permission unavailable` 或 `notification delivery unavailable` 语义。该约束不新增遥测 SDK、后台通知、自动更新或 H5 直连 Tauri JS 插件。
|
||||
|
||||
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-18 追加:H5 账号状态刷新开始消费 `app.reloadWebView`。用户登录成功、退出登录、其它身份边界变化或登录状态异常页点击重新尝试时,`AuthGate` 会优先请求 Expo 壳刷新当前 WebView;宿主未声明或刷新失败时再回退浏览器刷新,避免在移动壳内绕过受控容器刷新入口。
|
||||
|
||||
2026-06-18 追加:移动壳 WebView 内容 / 渲染进程终止时复用同一受控刷新路径。iOS `onContentProcessDidTerminate` 和 Android `onRenderProcessGone` 只调用当前 `react-native-webview` 的 `reload()`,不改写 H5 URL、不注入额外脚本、不新增宿主恢复页面,避免系统回收 WebView 进程后留下空白容器。
|
||||
|
||||
Reference in New Issue
Block a user