diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 45ec4e14..167fdc71 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -2,6 +2,8 @@ import fs from 'node:fs'; const configPath = new URL('../src-tauri/tauri.conf.json', import.meta.url); const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); +const packagePath = new URL('../package.json', import.meta.url); +const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const capabilityPath = new URL( '../src-tauri/capabilities/main.json', import.meta.url, @@ -20,6 +22,15 @@ const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8'); const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url); const main = fs.readFileSync(mainPath, 'utf8'); +function extractCargoPackageString(source, key) { + const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm')); + if (!match) { + throw new Error(`unable to read Cargo package ${key}`); + } + + return match[1]; +} + function extractStringArrayExport(source, exportName) { const match = source.match( new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`), @@ -83,6 +94,30 @@ if (unknownDesktopCapabilities.length > 0) { ); } +if (config.productName !== 'Genarrative') { + throw new Error('desktop shell productName must be Genarrative'); +} + +if (config.identifier !== 'world.genarrative.desktop') { + throw new Error('desktop shell identifier must be world.genarrative.desktop'); +} + +if (config.version !== '0.1.0') { + throw new Error('desktop shell app version must be 0.1.0'); +} + +if (packageConfig.version !== config.version) { + throw new Error('desktop shell package version must match tauri.conf.json version'); +} + +if (extractCargoPackageString(cargoManifest, 'name') !== 'genarrative-desktop-shell') { + throw new Error('desktop shell Cargo package name must be genarrative-desktop-shell'); +} + +if (extractCargoPackageString(cargoManifest, 'version') !== config.version) { + throw new Error('desktop shell Cargo package version must match tauri.conf.json version'); +} + if (config.build?.frontendDist !== '../../../dist') { throw new Error('desktop shell must package the root H5 dist'); } @@ -96,10 +131,45 @@ if (!mainWindow || mainWindow.create !== false) { throw new Error('desktop shell must create the main window from Rust setup'); } +if (String(mainWindow.url ?? '').startsWith('http')) { + throw new Error('desktop shell release window must load packaged H5 assets'); +} + +if (!String(mainWindow.url ?? '').startsWith('index.html?')) { + throw new Error('desktop shell release window must enter through packaged index.html'); +} + +if (!String(config.build?.devUrl ?? '').startsWith('http://127.0.0.1:3000/?')) { + throw new Error('desktop shell dev URL must load the local Vite H5 entry'); +} + if (!config.bundle?.icon?.includes('icons/icon.png')) { throw new Error('desktop shell must use the real brand icon asset'); } +if (config.bundle?.active !== true || config.bundle?.targets !== 'all') { + throw new Error('desktop shell bundle targets must remain enabled for all platforms'); +} + +const csp = String(config.app?.security?.csp ?? ''); +for (const blockedCspToken of ["'unsafe-eval'", 'tauri:', 'file:']) { + if (csp.includes(blockedCspToken)) { + throw new Error(`desktop shell CSP must not include ${blockedCspToken}`); + } +} + +for (const requiredCspToken of [ + "default-src 'self'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + 'connect-src', + 'frame-src', +]) { + if (!csp.includes(requiredCspToken)) { + throw new Error(`desktop shell CSP missing ${requiredCspToken}`); + } +} + const requiredUrlParts = [ 'clientRuntime=native_app', 'clientType=native_app', @@ -222,6 +292,13 @@ if (!cargoManifest.includes('tauri-plugin-single-instance = "2.4.2"')) { throw new Error('desktop shell must depend on tauri-plugin-single-instance'); } +if ( + cargoManifest.includes('tauri-plugin-updater') || + JSON.stringify(config).includes('updater') +) { + throw new Error('desktop shell must not configure updater without real signing and endpoint'); +} + if ( main.indexOf('tauri_plugin_single_instance::init') > main.indexOf('tauri_plugin_clipboard_manager::init()') diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index a540caa9..a2b56d31 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -51,6 +51,7 @@ - 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。 - 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 约束漂移。 - 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。 - 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。 - 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 6baa7358..5f5310e1 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -305,6 +305,8 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳启用 Tauri 单实例。用户重复启动桌面 App 时,新实例会退出并唤醒已有主窗口;该回调只执行显示、取消最小化和聚焦主窗口,不把第二实例的命令行参数、工作目录或深链内容作为事件透传给 H5。桌面深链若后续需要接入,必须单独定义受控来源、路径归一和 HostBridge / 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 禁用约束。 + ### Phase 4:宿主能力扩展 - 移动端接入系统分享、推送、原生登录和渠道支付。