From 242f872ac06a5e2fe8f6bf2e3a7671ca03ab4568 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 11:33:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E8=A7=82=E6=B5=8B=E6=B8=A0=E9=81=93=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面壳配置检查拒绝崩溃上报、analytics、遥测日志、自动更新和渠道分发 SDK 依赖。 桌面壳配置检查拒绝相关 Rust 初始化片段提前进入 Tauri 壳。 更新 Expo React Native 与 Tauri 宿主壳方案中的桌面观测渠道边界。 更新共享决策日志记录桌面壳观测与渠道 SDK 接入前置条件。 --- apps/desktop-shell/scripts/check-config.mjs | 102 ++++++++++++++++++ .../shared-memory/decision-log.md | 1 + ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 4 + 3 files changed, 107 insertions(+) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 1174a364f..722888878 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -45,6 +45,50 @@ const devScaffoldTerms = [ '模' + '拟', '伪' + '造', ]; +const blockedDesktopNpmDependencies = [ + '@amplitude/analytics-browser', + '@bugsnag/js', + '@datadog/browser-rum', + '@segment/analytics-next', + '@sentry/browser', + '@sentry/react', + '@sentry/tauri', + '@sentry/tracing', + '@tauri-apps/plugin-log', + '@tauri-apps/plugin-updater', + 'amplitude-js', + 'mixpanel-browser', + 'posthog-js', +]; +const blockedDesktopCargoDependencies = [ + 'bugsnag', + 'datadog', + 'opentelemetry', + 'opentelemetry-otlp', + 'posthog-rs', + 'sentry', + 'sentry-tauri', + 'tauri-plugin-log', + 'tauri-plugin-updater', + 'tracing-opentelemetry', +]; +const blockedDesktopSdkSnippets = [ + '@sentry/', + 'Bugsnag.start', + 'Datadog', + 'Sentry.init', + 'analytics.load', + 'amplitude.init', + 'datadogRum.init', + 'mixpanel.init', + 'opentelemetry::', + 'posthog.init', + 'sentry::init', + 'sentry_tauri', + 'tauri_plugin_log::', + 'tauri_plugin_updater::', + 'tracing_opentelemetry', +]; const requiredBundleIcons = [ 'icons/32x32.png', 'icons/128x128.png', @@ -69,6 +113,61 @@ function extractCargoPackageString(source, key) { return match[1]; } +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function assertNoBlockedNpmDependencies() { + const dependencySections = [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', + ]; + + for (const dependency of blockedDesktopNpmDependencies) { + for (const section of dependencySections) { + if (packageConfig[section]?.[dependency]) { + throw new Error( + `desktop shell must not depend on ${dependency} before real observability/channel contracts exist`, + ); + } + } + } +} + +function assertNoBlockedCargoDependencies() { + for (const dependency of blockedDesktopCargoDependencies) { + const dependencyPattern = new RegExp( + `^\\s*(?:"${escapeRegExp(dependency)}"|${escapeRegExp(dependency)})\\s*=`, + 'm', + ); + if (dependencyPattern.test(cargoManifest)) { + throw new Error( + `desktop shell must not depend on ${dependency} before real observability/channel contracts exist`, + ); + } + } +} + +function assertNoBlockedDesktopSdkSnippets() { + const sources = [ + ['tauri.conf.json', JSON.stringify(config)], + ['build.rs', buildScript], + ['main.rs', main], + ]; + + for (const [sourceName, source] of sources) { + for (const snippet of blockedDesktopSdkSnippets) { + if (source.includes(snippet)) { + throw new Error( + `desktop shell ${sourceName} must not initialize ${snippet} before real observability/channel contracts exist`, + ); + } + } + } +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -120,6 +219,9 @@ function assertNoDevScaffoldTerms(files) { assertNoDevScaffoldTerms( productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)), ); +assertNoBlockedNpmDependencies(); +assertNoBlockedCargoDependencies(); +assertNoBlockedDesktopSdkSnippets(); function readPngSize(file) { const buffer = fs.readFileSync(file); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 4e15f461c..ab1a2a5af 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -59,6 +59,7 @@ - 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘,必须通过托盘“退出”结束应用;托盘注册失败不得阻断主窗口启动,也不得拦截关闭,避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。 - 2026-06-18 Tauri 单实例:桌面壳启用 `tauri-plugin-single-instance` 并要求该插件最先注册;重复启动 App 时第二实例退出,只唤醒、取消最小化并聚焦已有主窗口,不把第二实例 argv / cwd / 深链内容作为事件透传给 H5。桌面深链后续如需接入,必须先定义受控 URL 归一和宿主边界,不能借单实例回调直接开放任意启动参数。 - 2026-06-18 桌面壳安装包身份:Tauri 桌面壳的产品名固定为 `Genarrative`,应用 identifier 固定为 `world.genarrative.desktop`,Tauri 配置、`apps/desktop-shell/package.json` 与 Cargo package 版本统一为 `0.1.0`;Release 主窗口只加载打包的 `index.html` 和根 `dist` H5 资产,dev URL 只指向本机 Vite 调试入口。桌面壳 CSP 保持 `script-src 'self'`,不得加入 `unsafe-eval`、`tauri:` 或 `file:`,也不得在没有真实端点、签名密钥和发布流程前配置 updater;检查脚本会拒绝包身份、版本、CSP 或 updater 约束漂移。 +- 2026-06-18 桌面壳观测与渠道 SDK 边界:Tauri 桌面壳默认不接入崩溃上报、analytics、遥测日志、自动更新或渠道分发 SDK;Sentry、Datadog、PostHog、Segment、Amplitude、Bugsnag、OpenTelemetry、Tauri log / updater 等 Node / Cargo 依赖和 Rust 初始化片段都会被配置检查拒绝。后续只有在真实端点、采集字段、用户授权、隐私披露、签名和发布流程确定后,才能按单项能力更新方案并接入。 - 2026-06-18 桌面壳 HostBridge 版本边界:Tauri release / dev 入口 URL 的 `hostVersion` 必须与 `tauri.conf.json`、`apps/desktop-shell/package.json` 和 Cargo package 版本一致;`host.getRuntime` 回包继续使用 `env!("CARGO_PKG_VERSION")`,配置检查会拒绝入口 query 版本、Tauri 配置版本或 Rust runtime 版本来源分叉。 - 2026-06-18 桌面壳运行时平台 query:Tauri 静态配置中的 `hostPlatform=unknown` 只作为跨平台构建模板值;Rust `setup` 手动创建主窗口前必须把入口 URL 改写为当前 `macos` / `windows` / `linux`,保证 H5 首屏 query 与 `host.getRuntime` 回读的平台一致。第二实例参数、外部 deep link 或 H5 自报值不得覆盖该字段;桌面壳测试和配置检查会拒绝绕过该归一流程。 - 2026-06-18 桌面壳顶层导航边界:Tauri 主 WebView 只允许打包资产 URL 和 `https://app.genarrative.world` 同源 H5 route 留在主窗口;外域 `http:` / `https:`、`mailto:`、`tel:` 导航与 `window.open` 请求交给系统 opener 后拒绝 WebView 留壳;`javascript:`、`file:` 等危险协议直接拒绝。该规则不进入 HostBridge capability,不开放 opener JS guest API,配置检查和 cargo test 覆盖导航策略。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 513ede8f4..6cd259118 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -181,6 +181,7 @@ Tauri 壳同样只负责桌面宿主能力,不承接玩法业务。 - 文件导出、作品卡保存、图片拖拽导入、系统托盘、自动更新等桌面能力按后续需求逐项开放;其中系统托盘属于桌面壳自有能力,不作为 HostBridge method 暴露给 H5。 - 主 WebView 默认拒绝网页自动下载或 `` 触发的落盘动作;用户保存文本、图片、音频等内容必须走已声明的 `file.exportText`、`file.exportImage`、`file.exportAudio` HostBridge method,由 Rust 侧执行 MIME、大小、文件名清洗和系统保存对话框确认。 - 主 WebView 显式关闭 DevTools,Cargo 不启用 Tauri `devtools` feature;本地调试通过普通浏览器和 Vite 完成,不把可分发桌面壳变成调试容器。 +- 崩溃上报、前端 analytics、桌面遥测日志、自动更新和渠道分发 SDK 都必须等真实端点、采集字段、用户同意、隐私策略、签名和发布流程确定后逐项接入;当前桌面壳不安装 Sentry、Datadog、PostHog、Segment、Amplitude、Bugsnag、OpenTelemetry、Tauri log / updater 等相关依赖。 桌面 release 和 dev 模式: @@ -234,6 +235,7 @@ GameBridge 禁止: - 壳能力声明与两端壳测试必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。声明的 capability 必须存在于共享 HostBridge 白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、Expo managed config、移动端 production bundle 和桌面 release 构建入口不得漂移。 - 登录和支付能力在真实 SDK、渠道流程、后端契约和失败回退全部落地前不得进入 Expo / Tauri capabilities,也不得写进入口 URL `hostCapabilities`;两端配置检查会拒绝 `auth.requestLogin` 和 `payment.request` 的伪声明。 - 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。 +- 桌面壳不得提前安装或初始化崩溃上报、analytics、遥测日志、自动更新或渠道分发 SDK;这类能力必须先补齐真实后端 / 第三方端点、采集口径、用户授权、隐私披露、签名和发布流程,再进入 Tauri 配置、Cargo 依赖、Node 依赖或 Rust 初始化代码。 - Tauri 禁止把 shell / fs 等高危插件作为默认能力暴露给主 WebView。 - Tauri 主 WebView 禁止默认下载落盘;桌面文件保存只能通过受控 HostBridge 导出能力进入系统保存对话框。 - Tauri 主 WebView 禁止默认打开 DevTools;不得通过配置或 Cargo feature 为分发壳启用浏览器检查器。 @@ -325,6 +327,8 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳安装包身份固定为 `world.genarrative.desktop`,产品名为 `Genarrative`,Tauri、Node package 与 Cargo package 版本统一为 `0.1.0`。Release 主窗口只能从打包进二进制的 `index.html` 进入根 `dist` H5 资产,dev URL 只能指向本机 Vite 调试入口;CSP 必须保持 `script-src 'self'`,不得加入 `unsafe-eval`、`tauri:` 或 `file:` 这类扩大桌面攻击面的来源。当前不配置自动更新器,直到存在真实更新端点、签名密钥和发布流程再接入;`apps/desktop-shell/scripts/check-config.mjs` 会校验这些包身份、版本、CSP 和 updater 禁用约束。 +2026-06-18 追加:桌面壳默认不接入崩溃上报、analytics、遥测日志或渠道分发 SDK。`apps/desktop-shell/scripts/check-config.mjs` 会拒绝 Sentry、Datadog、PostHog、Segment、Amplitude、Bugsnag、OpenTelemetry、Tauri log / updater 等 Node / Cargo 依赖和 Rust 初始化片段;后续只有在真实采集端点、数据字段、用户授权、隐私披露、签名和发布流程确定后,才能按单项能力补充方案与实现。 + 2026-06-18 追加:桌面壳 release / dev 入口 URL 的 `hostVersion` 必须与 Tauri `tauri.conf.json`、Node package 和 Cargo package 版本一致;`host.getRuntime` 回包继续使用 `env!("CARGO_PKG_VERSION")`,配置检查会拒绝入口 query 版本、Tauri 配置版本或 Rust runtime 版本来源分叉,避免桌面包升级时 H5 首屏上下文与 runtime 回读不一致。 2026-06-18 追加:桌面壳静态 Tauri 配置中的 `hostPlatform=unknown` 只作为跨平台构建模板值。Rust `setup` 手动创建主窗口前会把入口 URL 归一为当前 `macos` / `windows` / `linux`,保证 H5 首屏 query 与 `host.getRuntime` 回读的平台一致;不通过第二实例参数、外部 deep link 或 H5 自报值覆盖该平台字段。