diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 31a8e7c8f..788d3c4e4 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -13,7 +13,7 @@ const buildScriptPath = new URL('../src-tauri/build.rs', import.meta.url); const buildScript = fs.readFileSync(buildScriptPath, 'utf8'); const cargoManifestPath = new URL('../src-tauri/Cargo.toml', import.meta.url); const cargoManifest = fs.readFileSync(cargoManifestPath, 'utf8'); -const iconPath = new URL('../src-tauri/icons/icon.png', import.meta.url); +const iconDirPath = new URL('../src-tauri/icons/', import.meta.url); const sharedContractPath = new URL( '../../../packages/shared/src/contracts/hostBridge.ts', import.meta.url, @@ -41,6 +41,20 @@ const devScaffoldTerms = [ '模' + '拟', '伪' + '造', ]; +const requiredBundleIcons = [ + 'icons/32x32.png', + 'icons/128x128.png', + 'icons/128x128@2x.png', + 'icons/icon.icns', + 'icons/icon.ico', + 'icons/icon.png', +]; +const requiredPngIconSizes = new Map([ + ['32x32.png', 32], + ['128x128.png', 128], + ['128x128@2x.png', 256], + ['icon.png', 512], +]); function extractCargoPackageString(source, key) { const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm')); @@ -103,6 +117,68 @@ assertNoDevScaffoldTerms( productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)), ); +function readPngSize(file) { + const buffer = fs.readFileSync(file); + if ( + buffer.length < 24 || + buffer.toString('hex', 0, 8) !== '89504e470d0a1a0a' + ) { + throw new Error(`desktop shell icon is not a PNG: ${file.pathname}`); + } + + const width = buffer.readUInt32BE(16); + const height = buffer.readUInt32BE(20); + const dataStart = buffer.indexOf(Buffer.from('IDAT')); + const hasTransparencyChunk = buffer.includes(Buffer.from('tRNS')); + const hasAlphaColorType = buffer[25] === 4 || buffer[25] === 6; + + return { + width, + height, + dataStart, + hasTransparency: hasTransparencyChunk || hasAlphaColorType, + }; +} + +function assertDesktopIconSet() { + const icons = config.bundle?.icon ?? []; + if ( + icons.length !== requiredBundleIcons.length || + requiredBundleIcons.some((icon) => !icons.includes(icon)) + ) { + throw new Error('desktop shell must bundle the full real desktop icon set'); + } + + for (const [fileName, expectedSize] of requiredPngIconSizes) { + const icon = readPngSize(new URL(fileName, iconDirPath)); + if (icon.width !== expectedSize || icon.height !== expectedSize) { + throw new Error(`desktop shell icon ${fileName} must be ${expectedSize}x${expectedSize}`); + } + if (!icon.hasTransparency || icon.dataStart === -1) { + throw new Error(`desktop shell icon ${fileName} must use a real transparent brand asset`); + } + } + + const ico = fs.readFileSync(new URL('icon.ico', iconDirPath)); + if ( + ico.length < 6 || + ico.readUInt16LE(0) !== 0 || + ico.readUInt16LE(2) !== 1 || + ico.readUInt16LE(4) < 3 + ) { + throw new Error('desktop shell Windows icon must be a multi-size ICO'); + } + + const icns = fs.readFileSync(new URL('icon.icns', iconDirPath)); + if ( + icns.length < 16 || + icns.toString('ascii', 0, 4) !== 'icns' || + icns.readUInt32BE(4) !== icns.length + ) { + throw new Error('desktop shell macOS icon must be a valid ICNS'); + } +} + function extractStringArrayExport(source, exportName) { const match = source.match( new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`), @@ -215,9 +291,7 @@ 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'); -} +assertDesktopIconSet(); if (config.bundle?.active !== true || config.bundle?.targets !== 'all') { throw new Error('desktop shell bundle targets must remain enabled for all platforms'); @@ -382,11 +456,6 @@ if (main.includes('single-instance",') || main.includes('"single-instance"')) { throw new Error('desktop shell must not emit secondary-instance argv to H5'); } -const icon = fs.readFileSync(iconPath); -if (icon.length < 10000) { - throw new Error('desktop shell icon must use a real brand asset'); -} - for (const snippet of requiredMainSnippets) { if (!main.includes(snippet)) { throw new Error(`desktop shell Rust host bridge missing ${snippet}`); diff --git a/apps/desktop-shell/src-tauri/icons/128x128.png b/apps/desktop-shell/src-tauri/icons/128x128.png new file mode 100644 index 000000000..1d09353bb Binary files /dev/null and b/apps/desktop-shell/src-tauri/icons/128x128.png differ diff --git a/apps/desktop-shell/src-tauri/icons/128x128@2x.png b/apps/desktop-shell/src-tauri/icons/128x128@2x.png new file mode 100644 index 000000000..a301280b5 Binary files /dev/null and b/apps/desktop-shell/src-tauri/icons/128x128@2x.png differ diff --git a/apps/desktop-shell/src-tauri/icons/32x32.png b/apps/desktop-shell/src-tauri/icons/32x32.png new file mode 100644 index 000000000..ed8cd0c4b Binary files /dev/null and b/apps/desktop-shell/src-tauri/icons/32x32.png differ diff --git a/apps/desktop-shell/src-tauri/icons/icon.icns b/apps/desktop-shell/src-tauri/icons/icon.icns new file mode 100644 index 000000000..e73546ded Binary files /dev/null and b/apps/desktop-shell/src-tauri/icons/icon.icns differ diff --git a/apps/desktop-shell/src-tauri/icons/icon.ico b/apps/desktop-shell/src-tauri/icons/icon.ico new file mode 100644 index 000000000..735dbd072 Binary files /dev/null and b/apps/desktop-shell/src-tauri/icons/icon.ico differ diff --git a/apps/desktop-shell/src-tauri/tauri.conf.json b/apps/desktop-shell/src-tauri/tauri.conf.json index 25ce2983d..32d46765d 100644 --- a/apps/desktop-shell/src-tauri/tauri.conf.json +++ b/apps/desktop-shell/src-tauri/tauri.conf.json @@ -29,6 +29,13 @@ "bundle": { "active": true, "targets": "all", - "icon": ["icons/icon.png"] + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico", + "icons/icon.png" + ] } } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 613743c51..ff08316df 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -54,6 +54,7 @@ - 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 壳生产代码禁用临时替身:Expo 与 Tauri 壳的生产源码和配置不得出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 mock。两端壳配置检查会扫描生产入口、配置和壳实现,防止把临时替身、占位文案或伪实现带进可分发壳。 - 2026-06-18 移动壳启动页与 adaptive icon:Expo 移动壳启动页和 Android adaptive icon 复用现有真实品牌图标 `apps/mobile-shell/assets/icon.png`,背景色固定为 H5 壳根背景 `#fffdf9`。该 PNG 是 1024x1024 RGBA 透明前景品牌资产,不新增占位图;配置检查会校验图标尺寸、透明像素、splash 和 adaptive icon 指向,避免后续换成非品牌或占位素材。 +- 2026-06-18 桌面壳 bundle 图标集:Tauri 桌面壳从现有真实品牌 PNG `apps/desktop-shell/src-tauri/icons/icon.png` 派生 `32x32.png`、`128x128.png`、`128x128@2x.png`、`icon.ico` 和 `icon.icns`,并在 `bundle.icon` 中同时声明这些平台图标。检查脚本会校验 PNG 尺寸、ICO 多尺寸头部、ICNS 容器长度和 bundle 图标列表,避免后续退回单图标或替换为非品牌 / 占位素材。 - 影响范围:`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 6b8c7f769..591ffd049 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -311,6 +311,8 @@ GameBridge 禁止: 2026-06-18 追加:移动壳启动页与 Android adaptive icon 复用现有真实品牌图标 `apps/mobile-shell/assets/icon.png`,背景色固定为 H5 壳根背景 `#fffdf9`。该 PNG 是 1024x1024 RGBA 透明前景品牌资产,不新增占位图;Expo `splash` 使用同一图标 `contain` 展示,Android `adaptiveIcon.foregroundImage` 使用同一透明前景图,`check-config.mjs` 会校验图标尺寸、透明像素、启动页和 adaptive icon 配置。 +2026-06-18 追加:桌面壳 bundle 图标集从现有真实品牌 PNG `apps/desktop-shell/src-tauri/icons/icon.png` 派生,补齐 `32x32.png`、`128x128.png`、`128x128@2x.png`、`icon.ico` 和 `icon.icns`,Tauri `bundle.icon` 同时声明这些平台图标。没有引入外部素材或占位图;`apps/desktop-shell/scripts/check-config.mjs` 会校验 PNG 尺寸、ICO 多尺寸头部、ICNS 容器长度和 bundle 图标列表,避免后续退回单图标或替换为非品牌素材。 + ### Phase 4:宿主能力扩展 - 移动端接入系统分享、推送、原生登录和渠道支付。